Java Spring

La base técnica de las aplicaciones Jakarta EE

Este libro proporciona los elementos clave para orientarse en las diferentes tecnologías utilizadas en los proyectos basados ??en Spring. Tiene en cuenta las diferencias de configuración relacionadas con las versiones de Spring (en la versión 4.3 y 5.3 en el momento de la redacción del libro) y se basa en ejemplos concretos de uso. Permite rápidamente al lector una gran autonomía en un proyecto empresarial que utiliza Spring, ya sea durante las fases iniciales de un nuevo proyecto o para mantener un proyecto existente: comprensión del núcleo, acceso a los datos y control de la capa web. El conocimiento del desarrollo Java y, en particular, el desarrollo de aplicaciones web, es un requisito previo esencial para aprovechar al máximo el libro.

El autor presenta en primer lugar los elementos más sencillos y comunes de Spring (la configuración, los contextos, las librerías de terceros) y posteriormente, explica algunos aspectos más complejos que normalmente se encuentran en los proyectos (Recursos, Binders, Validadores, Conversores y pruebas). Se experimenta con la programación orientada a aspectos y se detallan las aplicaciones web Spring MVC y los Web Services, con las pruebas unitarias asociadas. El autor presenta las novedades Spring Boot, Kotlin con Angular, las aplicaciones orientadas a mensajes y Spring Batch, una introducción a Reactor y WebFlux y una descripción de la parte Spring de un proyecto generado a partir de JHipster para ilustrar una implementación muy actual, así como una presentación sobre el uso de GraphQL con Spring.
A lo largo de los capítulos, el autor se basa en ejemplos funcionales para permitir la experimentación lo antes posible por parte del lector. Para este propósito, los elementos están disponibles para su descarga en esta página.

Author(s)

Hervé LE MORVAN
En la actualidad, Hervé LE MORVAN es consultor DevOps para las principales firmas de los sectores bancario, seguros y de telecomunicaciones. Durante sus veinte años de experiencia, ha participado principalmente como referente técnico y formador en equipos de investigación y desarrollo (I+D) o arquitectos, y como apoyo al desarrollo de aplicaciones en intervenciones relacionadas con la migración o modernización de sistemas de información. En este sentido, ha estado involucrado en muchos proyectos de migración utilizando la plataforma Spring y conoce perfectamente las expectativas de los equipos en esta área. Toda esta experiencia es la que comparte voluntariamente a lo largo de las páginas de este libro.
Ref. ENI: EPT4JASP | ISBN: 9782409041082

Prólogo

Durante mis labores de consultoría, normalmente encontraba tediosas las fases de adquisición y asimilación de nuevas tecnologías.

Lo más habitual era que mis tareas consistieran en despejar un dominio para implementarlo posteriormente en proyectos concretos y formar a nuestros colaboradores. Algunos clientes usaban SQL ; otros, NoSQL, JSP, JSF, SinglePage Angular o React. Algunos tenían arquitecturas monolíticas y otros tenían microservicios. Unos utilizaban aplicaciones en Spring (Core) versión 2 o 3, mientras que otros habían sobrecargado Spring en los frameworks propietarios.

Para desarrollar habilidades y competencias, utilizo con frecuencia libros, especialmente los de la colección de ENI, los sitios web de editores de software y blogs especializados.

Los libros me dan una visión global y los sitios web me permiten aclarar ciertos puntos particulares.

Durante mis diferentes trabajos relacionados con Spring, tuve la gran oportunidad de recibir una ayuda fundamental de expertos en Spring y Jakarta EE, como Julien Dubois, Bernard Pons, Antonio Goncalves, Florent Ramière, Nicolas Romanetti y muchos otros, lo que me permitió implementar proyectos complejos.

También agradezco a Emmanuel Bernard (JBoss, Hibernate), Arnaud Héritier (CloudBees, Jenkins), Guillaume Laforge (Google, Groovy), Antonio Goncalves (Microsoft, autor), Vincent Massol (XWiki, Maven) y Audrey Neveu (Reactor/Spring, Devoxx4Kids) su participación en https://lescastcodeurs.com/, que es un podcast habitual en francés, realizado y producido por y para desarrolladores, que permite conocer las últimas noticias del ámbito Java y del desarrollo en general. También quería dar las gracias a los organizadores de Devoxx France y a todos sus colaboradores.

Las tecnologías Java han alcanzado tal grado de madurez que, en la actualidad, el desafío consiste principalmente en unir las piezas técnicas adecuadas para una necesidad específica. Algunas veces sucede que el ensamblado de conjuntos de frameworks preexistentes nos aleja del objetivo central de sus implementaciones. Es habitual que exista cierta saturación, por lo que la experimentación y las pruebas de carga siguen siendo las únicas formas de comprobar si la aplicación cumple lo que promete.

Paradójicamente, aunque la base aplicativa a menudo es compleja, es necesario que los desarrolladores tengan que dominar con rapidez la arquitectura y los procedimientos operativos para construir la aplicación. El desarrollador debe hacer malabarismos entre la codificación y el desarrollo de las competencias que acompañan a su desarrollo.

También es necesario ajustar constantemente la complejidad de la base y las recomendaciones, para hacer que las aplicaciones sean mantenibles.

En el mundo de la Web, la parte back tiene un ciclo de control de versiones (versioning) más lento, en comparación con la parte front. Intentamos tener un backend relativamente estable y a menudo genérico para poder centrar el esfuerzo en la parte front, que evoluciona rápidamente, integrando cada vez más capacidad de respuesta y complejidad, especialmente usando tecnologías basadas en JavaScript y sus derivados TypeScript y WebAssembly.

La capitalización de la experiencia se concreta y cristaliza gracias a través de la disponibilidad de generadores de código como jHipster (https://jhipster.github.io/) o Celerio (https://github.com/jaxio/celerio). Estas herramientas estructuran y armonizan proyectos. Permiten ahorrar un tiempo considerable durante las fases iniciales de los proyectos. De hecho, con una configuración relativamente modesta, construyen muy rápidamente un esqueleto de aplicación completo, que permite disponer de una aplicación que proporciona, a través de IHM (interfaces hombre-máquina) completas, un acceso a los datos en forma de CRUD (por Create, Read, Update, Delete). Es habitual que el acceso a los datos y su transporte a través de las diferentes capas se configure una única vez (plantillas o templates) y después se dupliquen para todas las entidades. SCRUD

Lo único que se debe hacer es adaptar las aplicaciones generadas para agrupar las páginas y personalizar el código. A cambio, estas herramientas requieren una adaptación muy rápida por parte de los colaboradores que, posteriormente, deben volver a trabajar el código generado para integrar las funcionalidades del negocio en una base compleja.

Esta fase, que consiste en renovar las IHM y añadir el código de negocio en una capa de servicio, es la fase del proyecto donde hay más valor añadido.

Estas herramientas son personalizables y es posible generar aplicaciones en capas tradicionales. A través de modificaciones importantes de los templates (plantillas) de creación, también es posible generar una parte importante de las aplicaciones respetando arquitecturas más modernas, como la arquitectura Hexagonal.

En la mayoría de los casos, para los clientes «tradicionales», hay un equipo de arquitectos que proporcionan la base técnica y aseguran el soporte. Este equipo forma parte de los proyectos durante la implementación y participa en el diseño de la primera versión de la aplicación. En las empresas que practican la agilidad a gran escala con el modelo de Spotify, hay equipos autónomos responsables de las elecciones técnicas y esto permite el máximo nivel de experimentación.

Escribí este libro con la ambición y el objetivo de proporcionar elementos que permitan a nuestros colaboradores desarrollar sus competencias, para que puedan adquirir de la manera más rápida posible la autonomía necesaria durante su participación en el desarrollo de aplicaciones para las que hay una base técnica preexistente.

Prólogo

Durante mis labores de consultoría, normalmente encontraba tediosas las fases de adquisición y asimilación de nuevas tecnologías.

Lo más habitual era que mis tareas consistieran en despejar un dominio para implementarlo posteriormente en proyectos concretos y formar a nuestros colaboradores. Algunos clientes usaban SQL ; otros, NoSQL, JSP, JSF, SinglePage Angular o React. Algunos tenían arquitecturas monolíticas y otros tenían microservicios. Unos utilizaban aplicaciones en Spring (Core) versión 2 o 3, mientras que otros habían sobrecargado Spring en los frameworks propietarios.

Para desarrollar habilidades y competencias, utilizo con frecuencia libros, especialmente los de la colección de ENI, los sitios web de editores de software y blogs especializados.

Los libros me dan una visión global y los sitios web me permiten aclarar ciertos puntos particulares.

Durante mis diferentes trabajos relacionados con Spring, tuve la gran oportunidad de recibir una ayuda fundamental de expertos en Spring y Jakarta EE, como Julien Dubois, Bernard Pons, Antonio Goncalves, Florent Ramière, Nicolas Romanetti y muchos otros, lo que me permitió implementar proyectos complejos.

También agradezco a Emmanuel Bernard (JBoss, Hibernate), Arnaud Héritier (CloudBees, Jenkins), Guillaume Laforge (Google, Groovy), Antonio Goncalves (Microsoft, autor), Vincent Massol (XWiki, Maven) y Audrey Neveu (Reactor/Spring, Devoxx4Kids) su participación en https://lescastcodeurs.com/, que es un podcast habitual en francés, realizado y producido por y para desarrolladores, que permite conocer las últimas noticias del ámbito Java y del desarrollo en general. También quería dar las gracias a los organizadores de Devoxx France y a todos sus colaboradores.

Las tecnologías Java han alcanzado tal grado de madurez que, en la actualidad, el desafío consiste principalmente en unir las piezas técnicas adecuadas para una necesidad específica. Algunas veces sucede que el ensamblado de conjuntos de frameworks preexistentes nos aleja del objetivo central de sus implementaciones. Es habitual que exista cierta saturación, por lo que la experimentación y las pruebas de carga siguen siendo las únicas formas de comprobar si la aplicación cumple lo que promete.

Paradójicamente, aunque la base aplicativa a menudo es compleja, es necesario que los desarrolladores tengan que dominar con rapidez la arquitectura y los procedimientos operativos para construir la aplicación. El desarrollador debe hacer malabarismos entre la codificación y el desarrollo de las competencias que acompañan a su desarrollo.

También es necesario ajustar constantemente la complejidad de la base y las recomendaciones, para hacer que las aplicaciones sean mantenibles.

En el mundo de la Web, la parte back tiene un ciclo de control de versiones (versioning) más lento, en comparación con la parte front. Intentamos tener un backend relativamente estable y a menudo genérico para poder centrar el esfuerzo en la parte front, que evoluciona rápidamente, integrando cada vez más capacidad de respuesta y complejidad, especialmente usando tecnologías basadas en JavaScript y sus derivados TypeScript y WebAssembly.

La capitalización de la experiencia se concreta y cristaliza gracias a través de la disponibilidad de generadores de código como jHipster (https://jhipster.github.io/) o Celerio (https://github.com/jaxio/celerio). Estas herramientas estructuran y armonizan proyectos. Permiten ahorrar un tiempo considerable durante las fases iniciales de los proyectos. De hecho, con una configuración relativamente modesta, construyen muy rápidamente un esqueleto de aplicación completo, que permite disponer de una aplicación que proporciona, a través de IHM (interfaces hombre-máquina) completas, un acceso a los datos en forma de CRUD (por Create, Read, Update, Delete). Es habitual que el acceso a los datos y su transporte a través de las diferentes capas se configure una única vez (plantillas o templates) y después se dupliquen para todas las entidades. SCRUD

Lo único que se debe hacer es adaptar las aplicaciones generadas para agrupar las páginas y personalizar el código. A cambio, estas herramientas requieren una adaptación muy rápida por parte de los colaboradores que, posteriormente, deben volver a trabajar el código generado para integrar las funcionalidades del negocio en una base compleja.

Esta fase, que consiste en renovar las IHM y añadir el código de negocio en una capa de servicio, es la fase del proyecto donde hay más valor añadido.

Estas herramientas son personalizables y es posible generar aplicaciones en capas tradicionales. A través de modificaciones importantes de los templates (plantillas) de creación, también es posible generar una parte importante de las aplicaciones respetando arquitecturas más modernas, como la arquitectura Hexagonal.

En la mayoría de los casos, para los clientes «tradicionales», hay un equipo de arquitectos que proporcionan la base técnica y aseguran el soporte. Este equipo forma parte de los proyectos durante la implementación y participa en el diseño de la primera versión de la aplicación. En las empresas que practican la agilidad a gran escala con el modelo de Spotify, hay equipos autónomos responsables de las elecciones técnicas y esto permite el máximo nivel de experimentación.

Escribí este libro con la ambición y el objetivo de proporcionar elementos que permitan a nuestros colaboradores desarrollar sus competencias, para que puedan adquirir de la manera más rápida posible la autonomía necesaria durante su participación en el desarrollo de aplicaciones para las que hay una base técnica preexistente.

Organización del libro

En primer lugar, este libro presenta una visión general de la arquitectura del framework. Posteriormente, utilizando algunos ejemplos, muestra que Spring tiene una orientación «design pattern». A continuación, presentamos algunos recordatorios sobre los elementos externos a Spring que se utilizan en los ejemplos. Seguidamente, nos sumergiremos en un primer nivel de detalle en el corazón del framework y después profundizaremos en él.

Abordaremos la programación orientada a aspectos que permite Spring. Estudiaremos la parte correspondiente a las pruebas de las aplicaciones Spring, que es particularmente potente en esta área. En primer lugar, estudiaremos en detalle la parte back de las aplicaciones y, en un segundo paso, presentaremos el front-end web y los web services.

Continuaremos con la integración de JSF2 y Angular en aplicaciones web Spring y terminaremos con una introducción a las nuevas API responsivas y diseccionaremos los backends generados con la herramienta jHipster.

Cada parte estará ilustrada con ejemplos y propondremos algunos ejercicios. Los ejemplos y la solución a estos ejercicios se pueden descargar en la página Información. Si experimenta con estos ejemplos y ejercicios, se podrá familiarizar más rápidamente con el framework. De hecho, la manera de mejorar su eficacia y efectividad es a través de la experimentación con este framework.

La complejidad es progresiva a medida que se avanza en la lectura de los capítulos. Algunos aspectos se muestran de manera simplificada y se profundiza en ellos más adelante.

VM6393:8

Público objetivo

Cualquiera que trabaje en un proyecto que utilice Spring puede sacar provecho de este libro. El programador novato en Spring tendrá una visión general de lo que este framework puede hacer y podrá practicar para aprender más. El líder técnico o tech lead y el arquitecto podrán tomar algunas ideas y reutilizar el contenido para explicar a los equipos los fundamentos de esta tecnología. Si desea practicar y profundizar, también puede utilizar la formación que ENI pone a su alcance.

VM6393:8

Por qué Spring

Principalmente, Spring permite hacer lo mismo que haría con un servidor Jakarta EE completo (Java en su versión enterprise) como WebSphere, WebLogic, JBoss, Glassfish, pero también con un servidor web ligero (contenedor) como Tomcat, Undertow o Jetty, y de una manera mucho más sencilla y verificable. En términos de funcionalidades, generalmente está por delante de Jakarta EE. En principio, se diseñó para paliar algunas de las dificultades encontradas durante la implementación de las primeras versiones de Java EE, sobre todo con los EJB, y con posterioridad amplió de manera gradual la diferencia entre ellos, evolucionando más rápidamente.

Spring permite interfasar de manera sencilla con varios frameworks de terceros, utilizando API unificadas. Difumina las diferencias entre frameworks, porque ofrece API que simplifican el código. Por ejemplo, las operaciones con datos utilizando bases de datos SQL son genéricas y reutilizables. La idea es estandarizar las llamadas entre frameworks, así como simplificar y estandarizar el manejo de errores y excepciones.

También se distingue por su capacidad para probar aplicaciones de manera sencilla y eficiente al incorporar en las pruebas una versión ligera del contexto de ejecución, mientras que en Jakarta EE normalmente se requiere un servidor dedicado. Solo JBoss se acerca a las pruebas de Arquillian (http://arquillian.org/), así como a su disponibilidad del Embedded JBoss.

VM6393:8

Requisitos previos para abordar Spring y Jakarta EE

La principal ambición del framework Spring es proporcionar una arquitectura que permita ofrecer la funcionalidad de Jakarta EE en un contexto de contenedor web sencillo y ligero. Jakarta EE es el nuevo nombre para Java Enterprise Edition que en su momento eligió la Fundación Eclipse, ya que Java es un nombre registrado.

Por lo tanto, para usarlo correctamente, es necesario entender los mecanismos Jakarta EE. Hoy en día, podemos considerar que, para poder confiar en un consultor, idealmente debe conocer tanto Spring como Jakarta EE.

Para aprovechar al máximo este libro, es necesario conocer en la medida de lo posible la programación Java en general y, en particular, Java web en su versión clásica, utilizando servlets, servidores SOAP o REST, páginas JSP o JSF:

  1. nociones de JavaBeans,

  2. colecciones,

  3. bases de datos con JDBC,

  4. conceptos básicos de escritura de archivos XML.

En cualquier caso, en este libro se proporcionan recordatorios e información que lo guiarán en estas áreas.

Un ordenador con conexión a Internet permite cargar los ejemplos y ejercicios en la web de Ediciones ENI.

Los ejemplos se ilustran con fragmentos de proyectos funcionales que pueden servir como base para experimentar y aprender el framework a través de casos de uso reales.

También se presentan ejemplos que utilizan Angular, pero más bien con fines informativos, para ilustrar la parte back y sus interacciones con la parte front. El tema está muy bien tratado en el libro Angular - Desarrolle sus aplicaciones con el framework JavaScript de Google (2.ª edición), de Daniel DJORDJEVIC, Sébastien OLLIVIER y William KLEIN, publicado por Ediciones ENI.

Java utiliza la JVM como entorno de ejecución. Ahora también es posible usar la JVM con lenguajes de programación que la utilizan y amplían, como Groovy, Kotlin, Scala, Ceylon, Clojure, JRuby, Jython. Spring está escrito en Java y, por lo tanto, también es posible usar Spring con estos lenguajes. Spring ha integrado Kotlin, como veremos en un capítulo dedicado. Kotlin

Jakarta EE es un conjunto de estándares para componentes de software que se pueden utilizar en un servidor Jakarta EE. La decisión de Oracle de confiar estos estándares a la Fundación Eclipse no está exenta de consecuencias. Algunos JSR (Java Specification Requests) han sido anulados y ahora depende de la comunidad open source dar vida a estas especificaciones.

Hasta ahora, Spring y Jakarta EE han estado compitiendo. Los principales editores de servidores Jakarta EE siguen confiando y piensan que estas especificaciones continuarán evolucionando. Por lo tanto, Spring continuará abstrayendo y encapsulando una serie de elementos de Java EE y Jakarta EE.

VM6393:8

Objetivos del libro

El objetivo del libro es ver en detalle los fundamentos del framework Spring. Se revisan los módulos más utilizados. El uso de módulos no descritos en este libro será fácil de abordar porque Spring sigue un conjunto de buenas prácticas.

Este libro se ha estructurado para aportar rápidamente los elementos necesarios para ser autónomo en la creación o mantenimiento de una aplicación basada en Spring, gracias a la comprensión del núcleo, el acceso a los datos y el dominio de las capas web.

VM6393:8

Introducción

En primer lugar, este capítulo presenta Spring en su contexto histórico y, seguidamente, describe sus partes constitutivas y elementos de configuración.

Aspectos históricos

El framework Spring se diseñó para facilitar la creación de aplicaciones empresariales en un contenedor web ligero como Tomcat, es decir, sin administración de EJB.

En las siguientes tablas se proporciona una ubicación temporal.

Evolución de Java Enterprise

Año

Versión

Contenido

1999

J2EE 1.2

Servets, JSP, EJB (1.1), JMS, RMI

2001

J2EE 1.3

EJB (2.0 CMP), JCA

2003

J2EE 1.4

EJB (2.1 MDB), Web services, JAX-RPC, Deployment Specification

2006

Java EE 5

EJB (3.0 annotations), JPA, Annotations, JSF, JAX-WS

2009

Java EE 6

Profil web, Servlet 3.0, EJB (3.1 singletons, lite, asynchronous), CDI, JAX-RS

2013

Java EE 7

JMS 2.0, JAX-RS 2.0, Servlet 3.1, API JSON, WebSockets

2017

Java EE 8

CDI 2.0, JSON-B 1.0, Servlet 4.0, JAX-RS 2.1, JSF 2.3, JSON-P 1.1, Bean Validation 2.0, JPA 2.2, WebSocket 1.6

Poco después del lanzamiento de Java, aparecieron los principios de diseño basados en JavaBeans que, por ejemplo, se utilizarían posteriormente en librerías gráficas de Java, como AWT. Sin embargo, no existía nada que permitiera usarlo en el lado del servidor de aplicaciones. Se hace un intento con EJB 1, pero nos alejamos mucho de los aspectos principales, y la evolución hacia EJB 2 complicaría aún más las cosas.

En ese momento, las JVM eran de pequeño tamaño, lo que hacía necesario que se multiplicaran y se comunicaran entre sí para obtener una JVM más grande (compuesta por nodos). Una JVM utiliza prácticamente toda la RAM de una máquina. Se agrupaban las máquinas en clústeres especializados y organizando nodos, lo que en última instancia permitía disponer de más espacio de memoria y capacidad de cálculo (CPU). Es habitual que haya un nodo maestro supervisando varios nodos secundarios.

En 2002, la situación era muy especial. No se podía ser arquitecto de Java si no se codificaba con EJB 2.0, que era una puesta al día de EJB 1.1. Sin embargo, esta tecnología no estaba madura y muy pocos proyectos terminaron en producción. La complejidad de la aplicación era extrema. Las cosas funcionaron a pequeña escala, pero el aumento de la carga planteó muchos problemas, muy complejos de gestionar. Se intentó anticipar y evitar los problemas usando la metodología, pero se carecía de soluciones viables. La literatura de la época lo atestigua.

Se tenía un modelo de aplicación dominante, basado en aplicaciones cliente/servidor generadas en C, llamadas 3-tiers. Estas aplicaciones tenían una primera capa de presentación (front) compuesta por pantallas de una aplicación Windows. Una segunda capa de procesamiento (back) en forma de aplicación de servidor y una tercera capa de acceso a datos. Estos dos primeros tiers se comunicaban a través de un bus middleware como Corba o Tuxedo. A menudo, el servidor usaba una base de datos Oracle, con procedimientos almacenados. Teníamos TIS (talleres de ingeniería de software) como NatStar, PowerBuilder o Uniface, que permitían construir aplicaciones end to end. Otro modelo utilizaba mainframes, a menudo IBM, junto con bases de datos DB2, que se usaba de manera habitual. Era necesario interconectar las aplicaciones J2EE con las capacidades de la aplicación, a través de conectores propietarios que no dejaban mucho margen de maniobra para elegir el distribuidor del servidor J2EE.

Cabe señalar que no debe añadir J2EE o J2ee en su currículum, y tenga cuidado con los anuncios que solicitan un desarrollador J2EE. En la misma línea, si especifica Jakarta EE, le preguntarán si tiene experiencia en servidores Java que no utilizan Spring en favor de los componentes estándar de Jakarta EE (EJB, JCA, JNDI, SAAJ, etc.).

Los EJB 1 y 2 están diseñados en un modelo de capas y en tiers, con contenedores separados, donde estas capas generalmente están codificadas a mano. Los servidores de aplicaciones J2EE proporcionaban muchos servicios, pero eran complejos de administrar. Además, las aplicaciones eran difíciles de codificar e imposibles de probar de manera individual.

En 2002, Ron B. Johnson, un especialista australiano en TI, intentó formalizar una metodología para el desarrollo de aplicaciones J2EE en su primer libro, Expert One-2-One J2EE Design and Development. Expuso patterns de arquitectura y propuso un framework para resolver ciertos problemas. Este framework era una posible implementación para administrar JavaBeans. Enmarca la instanciación de objetos singleton o prototipo, la comunicación a través de notificaciones entre estos objetos basada en la AOP, así como una gestión completa del ciclo de vida de estos objetos. Trataremos todos estos aspectos en el resto de este libro.

El libro tuvo éxito y Ron se asoció con Juergen Hoeller, otro científico informático, para mejorar el framework del libro y convertirlo en open source en Sourceforge (https://sourceforge.net/projects/springframework/files/springframework/0.9/spring-framework-0.9.zip/download). Se creó una versión preliminar Spring 0.9 con ayuda de la comunidad y en 2004 se lanzó la versión 1.0 (https://github.com/selfpoised/spring-framework-1.0). Una gran mayoría de los conceptos y objetos de este framework todavía existen hoy en día en Spring 5 y 6.

Evolución de Spring

Año

Datos clave

2002

Expert One-2-One J2EE Design and Development

2004

Spring V1.0 bajo licencia Apache

2005

Alternative con EJB 2.x

2006

Jolt Productivity Award

2007

Spring 2.5 con anotaciones

2009

VMware compra SpringSource

2013

Pivotal: Joint venture VMware y EMC Corporation

Fechas de lanzamiento principales de Spring

Versión

Fecha

Expert One-on-One J2EE Design and Development

Octubre de 2002

Licence Apache

Junio de 2003

Milestone release, 1.0

Marzo de 2004

1.2.6

Noviembre de 2005

2.0

Octubre de 2006

2.5

Noviembre de 2007

3.0 GA

Diciembre de 2009

3.1 GA

Diciembre de 2011

3.2 GA

Diciembre de 2012

4.0 GA

Diciembre de 2013

4.1.8

Septiembre de 2015

4.2 GA

Julio de 2015

4.3

Junio de 2016

5.0.1

Octubre de 2017

5.3.18

31 de marzo de 2022

La versión 6 de Spring salió a finales de 2022.

Utilidad de Spring en un proyecto

El framework Spring es una solución ligera, basada en componentes de software independientes. Proporciona soluciones técnicas unificadas para la mayoría de las dificultades estándares que aparecen en los proyectos. Ofrece implementaciones para un conjunto de design patterns (patrones de diseño), como veremos en un capítulo dedicado a este tema. Las librerías de extensión también proporcionan facilidades para el uso de las librerías de software más utilizadas. Construir servicios web SOAP, servicios REST para acceder a los datos y hacer una aplicación basada en una sucesión de pantallas (wizard) se convierte en algo sencillo de hacer. Las diferencias entre las implementaciones de frameworks estándares de acceso a datos se suavizan con el uso de una API unificada.

Los proyectos se centran en la parte de negocio, mientras que la parte técnica la administra el framework Spring, que originariamente fue diseñado para Java, pero hoy en día existe para otras plataformas como .NET.

Spring tiene licencia en Apache 2.0 y es open source. Se puede acceder a las fuentes y hacer peticiones de evoluciones desde la URL https://github.com/spring-projects. Incluso es posible proponer correcciones de errores o cambios a través de «change requests» (peticiones de cambio).

El conjunto se maveniza (con las fuentes y el javadoc) en el repository central Maven para la versión estándar y en los repositories Maven SpringSource para las versiones en desarrollo. Encontraremos la ubicación de los repositorios al final del capítulo. Es posible usar Maven o Gradle para hacer proyectos de Spring. En este libro, utilizamos principalmente versiones Maven. El framework Spring es una solución full stack, es decir, que incluye todos los componentes técnicos y permite realizar aplicaciones completas, llave en mano. El núcleo de Spring tiene muy pocas dependencias externas. Al ser modular, permite incorporar solo los elementos necesarios para la aplicación, a diferencia de las soluciones basadas en servidores «pesados», que proporcionan un conjunto completo de módulos. De hecho, un servidor Jakarta EE debe ofrecer un conjunto de funcionalidades potencialmente utilizables por las aplicaciones que aloja, mientras que Spring solo incorpora lo que se utiliza.

Servidor JEE

Batch

Dependency Injection

JACC

JAXR

JSLT

Management

Bean Validation

Deployment

JASPIC

JMS

JTA

Servlet

CDI

EJB

JAX-RPC

JSF

JPA

Web Services

Common Annotations

EL

JAX-RS

JSON-P

JavaMail

Web Services Metadata

Concurrency EE

Interceptors

JAX-WS

JSP

Managed Beans

WebSocket

Connector

JSP Debugging

JAXB

JSON-B

Security

Connectors

Spring:

images/Spring.png

Spring es flexible y permite el uso gradual de recursos cada vez más complejos en función de la escalabilidad y la necesidad de compartir recursos entre múltiples aplicaciones. Ofrece una alternativa a los frameworks históricos como Struts, reutilizando su fuerza al tiempo que aporta evoluciones que hacen que el diseño de aplicaciones MVC sea más sencillo, robusto y menos acoplado.

Spring MVC es una alternativa al uso de Struts MVC. Si necesita trabajar en una aplicación Struts que no migró a Spring como debería, debe intentar negociar una migración.

Como veremos, Spring permite dar soporte a la programación orientada a aspectos, ayuda al uso de transacciones y facilita la persistencia de datos. Encapsula los procesamientos de forma no intrusiva. En la mayoría de los casos, el código no está relacionado con Spring. Spring conecta los elementos de negocio entre ellos y les proporciona un acceso sencillo a los elementos técnicos externos. Este framework es esencialmente un facilitador que ofrece la forma más sencilla de proporcionar el entorno técnico necesario para obtener una funcionalidad centrada en el negocio.

Hoy en día, Spring Boot se utiliza para crear proyectos que entran en producción. Para las aplicaciones web (o servidor), Spring se puede incorporar en un contenedor web ligero, como Tomcat o Jetty, o en un servidor web Jakarta EE, como WildFly (o JBoss Application Server) WebLogic, GlassFish o WebSphere. Entonces, nos podemos conformar con el perfil web de estos servidores. A menudo usamos contenedores ligeros como jBoss en un entorno dockerizado porque ofrece beneficios para el despliegue y la supervisión.

También es posible integrar Spring en una aplicación full Jakarta EE, pero un gran número de funcionalidades son redundantes y la utilidad del servidor full Jakarta EE se vuelve más específica, como beneficiarse del acceso a través de conectores «propietarios» a sistemas externos, que hacen que elementos que forman parte del histórico de la aplicación, como mainframes, services Tuxedo, ETL, etc., interactúen con la aplicación web.

En un servidor con un perfil full Jakarta EE, la contribución de Spring se centra en una simplificación si se compara con el uso de componentes de Jakarta EE aislados. Es más fácil desplegar una aplicación web como un WAR que como un conjunto de EAR. La configuración se puede incluir en la aplicación y la aplicación web se puede conectar muy fácilmente usando JNDI a recursos Jakarta EE, como datasources, pools de conexiones, JMS o cualquier otro elemento. Las pruebas unitarias y de integración se pueden realizar, en parte, en un contenedor que sea más ligero que todo el servidor. En una visión simplificadora, por servidores Java EE nos referimos a servidores históricos J2EE, JEE y Jakarta EE.

Por lo tanto, podemos desplegar fácilmente aplicaciones de Spring en un servidor Jakarta EE. El uso de perfiles desde Java EE 6 mitiga las diferencias y posibilita utilizar únicamente las funcionalidades web que tenemos en un Tomcat o un Jetty, dentro de un completo servidor de aplicaciones.

El hecho de incorporar todos los módulos provocaba que el servidor consumiera recursos que no se estaban utilizando. Cabe señalar, a este respecto, que el servidor JBoss siempre ha proporcionado una alternativa a los servidores Java EE clásicos. De hecho, propuso con mucha antelación los profiles, una modularidad hiperfina y un control total del servidor.

Spring se utiliza principalmente con Java, en su versión clásica, para el diseño de aplicaciones web o aplicaciones Batch, pero también comienza a funcionar en plataformas móviles con Android, por ejemplo. También es posible tener Spring con .NET con Spring.net y un uso conjunto de Nhibernate, que es la versión .NET de Hibernate. Este tándem hace que sea muy fácil crear aplicaciones .NET. El framework Spring basado en JVM Java también se puede utilizar con Groovy gracias al soporte mejorado desde Spring 4. Esta mejora permite utilizar toda la potencia de Spring con toda la modernidad del código Groovy.

El uso de Spring Kotlin se aborda en el capítulo Spring y Kotlin.

Spring es modular. Tiene un núcleo llamado Spring Core y librerías satélite independientes. Spring se puede utilizar en aplicaciones standalone (autónomas), aplicaciones de servidor o aplicaciones Batch. Los usos standalone pueden ser aplicaciones de línea de comandos, aplicaciones con HMI (interfaces hombre-máquina) con ventanas normalmente basadas en Swing o JavaFX (pero esto es cada vez menos frecuente).

En la actualidad, Spring Boot permite preconfigurar la aplicación y soporta la combinación de los diferentes frameworks utilizados. Sin embargo, para aplicaciones más tradicionales que no usan Spring Boot, debe elegir sus versiones de framework.

Hay un truco para elegir sus versiones de framework. Es suficiente con crear una aplicación Spring Boot equivalente, dejar que Spring elija sus versiones de dependencia, listarlas y llevarlas a la configuración de Spring sin Spring Boot.

Visión general y temas tratados en el libro

1. Los módulos Spring Módulo

Hasta la fecha, la documentación de Spring lista unos veinte módulos Spring agrupados por temas. Para los ejemplos de este libro, se utilizan las versiones 4.3 y 5.3.

El núcleo central en las versiones 4.3.30 y 5.3.18

Nombre del módulo

Utilidad

spring-core

Los fundamentos: gestión de objetos y proxy.

spring-beans

La configuración y las factories de beans.

spring-context

Gestión del contexto, EJB, JMX, JNDI, scheduling, validation.

spring-context-support

Support ehcache, guava, mail, scheduling UI.

spring-expression

Expresssion language Spring: SpEL.

La POA

Nombre del módulo

Utilidad

spring-aop

POA a través de proxys.

spring-aspects

Aspectos basados en AspectJ.

spring-instrument

Agente de instrumentación para el bootstrapping de la JVM.

spring-instrument-tomcat

Agente de instrumentación para Tomcat.

Mensajes

Nombre del módulo

Utilidad

spring-messaging

Tener aplicaciones basadas en mensajes.

Acceso a los datos

Nombre del módulo

Utilidad

spring-jdbc

Facilidades JDBC.

spring-tx

Gestión programática de transacciones.

spring-orm

Mapping Hibernate (JPA y JDO).

spring-oxm

Mapping Object/XML como JAXB, Castor, XMLBeans, JiBX y XStream.

spring-jms

Módulo Java Messaging Service e integración spring-messaging desde Spring Framework 4.1.

Web

Nombre del módulo

Utilidad

spring-web

Componentes web.

spring-webmvc

Web services REST y web MVC.

spring-websocket

Web sockets.

spring-webmvc-portlet

Web en un contexto portlet.

Prueba

Nombre del módulo

Utilidad

spring-test

Ayuda para utilizar JUnit y TestNG con Spring y objetos mock.

2. Temas tratados

Como vimos en el prólogo, Spring es muy extenso. No podemos cubrir todo en un libro, aunque vamos a tratar los temas más utilizados, a saber, la parte back de las aplicaciones web y, a menudo, móviles, así como la parte front, para la que nos vamos a limitar a las aplicaciones web, ya que incluiremos los servicios web en esta parte. Describiremos tanto como sea posible las pruebas unitarias asociadas. Los ejemplos están tomados de casos reales presentes en los proyectos que se pueden descargar. También se detalla la programación por aspectos, que se utiliza ampliamente con Spring.

Los ejemplos utilizan la última versión «release» de Spring, disponible en el momento de escribir este libro, es decir, la versión 4.3.30 y la versión 5.3.18. Se basan en el uso de Java 17 y están en UTF-8. Los ejemplos están mavenizados para facilitar el manejo. Los componentes de Jakarta EE se corresponden con los de Jakarta EE 8, es decir, JSF2, JPA2, Servlet 3 (y 4), etc.

3. Versión de los componentes de Spring utilizados en el libro

Extracto de las versiones utilizadas

Módulo

Versión

Utilidad

Spring Framework

4.3.30 y 5.3.18

Núcleo de Spring

Spring Security

4.2.20 y 5.6.42

Gestión de la seguridad

Spring Data Commons

1.13.23 y 2.6.3

Base de acceso rápido a los datos

4. Versión de recursos externos

Lista de elementos externos clave

Librería

Versión

Lombok

1.18.22

JUnit

4.13.2 y 5.8.x

fest-assert

1.4

easymock

1.3

logback-classic

1.2.11

H2

2.1.212

Hibernate

6.0.0.Final

apache commons-dbcp2

2.9.0

jackson

2.13.2

jsonpath

2.7.0

5. Gestión de dependencias de Maven Maven:dependencias

Los ejemplos utilizan el mecanismo clásico para las dependencias.

Solo utilizamos versiones de la última release (versión identificada como estable): 

<dependency> 
        <groupId>org.springframework</groupId> 
        <artifactId>spring-core</artifactId> 
        <version>4.3.30.RELEASE</version> 
      </dependency> 

o

<dependency> 
        <groupId>org.springframework</groupId> 
        <artifactId>spring-core</artifactId> 
        <version>5.3.18.RELEASE</version> 
      </dependency> 

El ejemplo de este capítulo muestra una configuración con todos los módulos y otras versiones comentadas.

Para la versión estándar, no podemos especificar un repositorio porque se trata de una versión generalizada en repositorios estándares Maven, pero también podemos especificar de manera precisa que deseamos que la versión se aloje en repositorios de Spring:

<repositories> 
          <repository> 
              <id>io.spring.repo.maven.release</id> 
              <url>http://repo.spring.io/release/</url> 
              <snapshots><enabled>false</enabled></snapshots> 
          </repository> 
      </repositories> 

Durante las fases de vigilancia tecnológica, puede ser necesario utilizar versiones que aún no están disponibles en los repositorios estándares.

Para la versión milestone:

<repositories> 
          <repository> 
              <id>io.spring.repo.maven.milestone</id> 
              <url>http://repo.spring.io/milestone/</url> 
              <snapshots><enabled>false</enabled></snapshots> 
          </repository> 
      </repositories> 

Y para las snapshot:

<repositories> 
          <repository> 
              <id>io.spring.repo.maven.snapshot</id> 
              <url>http://repo.spring.io/snapshot/</url> 
              <snapshots><enabled>true</enabled></snapshots> 
          </repository> 
      </repositories> 

6. Usar una BOM (Bill Of Materials) Maven Maven:BOM BOM

Una BOM Maven se remonta a la versión 2.0.9 de Maven y utiliza la noción de scope import.

Usamos los scopes Maven para actuar sobre el scope de nuestras dependencias.

El scope test permite, por ejemplo, importar las dependencias de los módulos utilizados por las pruebas solo durante la fase de test.

El scope import, que se utiliza en la sección dependencyManagement con una dependencia de tipo pom.xml, permite importar dependencias desde otro POM.

Por ejemplo:

Para el POM del proyecto1:

<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>fr.ediciones-eni</groupId> 
        <artifactId>pomA</artifactId> 
        <version>1</version> 
        <packaging>pom</packaging> 
       
         <dependencyManagement> 
          <dependencies> 
            <dependency> 
              <groupId>org.springframework</groupId> 
              <artifactId>spring-core</artifactId> 
              <version>4.3.30.RELEASE</version> 
             </dependency> 
          </dependencies> 
        </dependencyManagement> 
      </project> 

y para el POM del proyecto:

<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>fr.ediciones-eni</groupId> 
        <artifactId>pomB</artifactId> 
        <version>1</version> 
        <packaging>jar</packaging> 
       
        <dependencyManagement> 
          <dependencies> 
            <dependency> 
              <groupId>fr.ediciones-eni</groupId> 
              <artifactId>pomA</artifactId> 
              <version>1</version> 
              <scope>import</scope> 
              <type>pom</type> 
            </dependency> 
          </dependencies> 
        </dependencyManagement> 
      </project> 

Maven creará un POM compuesto como este:

<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>fr.ediciones-eni</groupId> 
        <artifactId>pomB</artifactId> 
        <version>1</version> 
        <packaging>jar</packaging> 
       
        <dependencyManagement> 
          <dependencies> 
            <dependency> 
              <groupId>org.springframework</groupId> 
              <artifactId>spring-core</artifactId> 
              <version>4.3.28.RELEASE</version> 
            </dependency> 
          </dependencies> 
         </dependencyManagement> 
      </project> 

BOM utiliza esta capacidad.

Una BOM es un POM BOM, que solo contiene una sección dependencyManagement.

Por ejemplo:

<dependency> 
         <groupId>org.springframework</groupId> 
         <artifactId>spring-core</artifactId> 
      </dependency> 

da:

<dependency> 
         <groupId>org.springframework</groupId> 
         <artifactId>spring-core</artifactId> 
         <version>4.3.30.RELEASE</version> 
      </dependency> 

Con esto se supera la limitación de Maven de disponer de un único POM padre.

La BOM garantiza que todas las dependencias directas y transitivas utilicen las mismas versiones.

La BOM utiliza los POM anidados y permite importar información desde otro POM. Definimos en la BOM todas las versiones de las dependencias que queremos utilizar en nuestros proyectos.

Los POM ya no contienen versiones para dependencias porque reutilizan la versión de la BOM.

<dependency> 
          <groupId>org.springframework</groupId> 
          <artifactId>spring-framework-bom</artifactId> 
          <version>${spring.version}</version> 
          <type>pom</type> 
          <scope>import</scope> 
      </dependency> 

Una ventaja adicional de utilizar la BOM es que ya no se puede usar el atributo <version>.

<dependencies> 
          <dependency> 
              <groupId>org.springframework</groupId> 
              <artifactId>spring-context</artifactId> 
          </dependency> 
          <dependency> 
              <groupId>org.springframework</groupId> 
              <artifactId>spring-web</artifactId> 
          </dependency> 
      <dependencies> 

En este libro, no utilizamos las BOM, para simplificar tanto como sea posible los archivos de configuración pom.xml.

Las empresas utilizan masivamente las BOM para actualizar todas las dependencias a la vez, pero en ocasiones esto dificulta la legibilidad. Existe un comando maven para listar todas las dependencias maven de un proyecto:

$ mvn dependency:tree 

Un capítulo dedicado explicará los conceptos básicos del uso de proyectos mavenizados, para permitir que las personas que aún no están familiarizadas con este modo de funcionamiento aprovechen al máximo los ejemplos.

Complementos

Los curiosos pueden descargar una de las primeras versiones de Spring en la versión 0.9, en la dirección https://sourceforge.net/projects/springframework/files/springframework/0.9/spring-framework-0.9.zip/download, del 26 de junio de 2003, con JDK 1.4 (JDK 1.5). Esta versión es compatible con JDK 1.4 parcheando este JDK para tener las anotaciones antes de la fase.

También está disponible la versión 1.0 y se compila con modificaciones menores.

En estas versiones encontramos la esencia misma de Spring.

Puntos clave

  • El framework Spring es modular.

  • Usamos Maven para configurarlo.

  • Las aplicaciones web y las pruebas son los dos puntos fuertes de Spring.

Introducción Design patterns

En el mundo Java actual, es relativamente raro tener que escribir una funcionalidad técnica que sea completamente nueva. En su lugar, ensamblamos componentes ya construidos, basándonos en frameworks y librerías open source. Cada vez más se trata de implementar componentes funcionales reutilizables para agrupar y compartir los desarrollos tanto como sea posible. Por lo tanto, por una parte, debemos tener cuidado de no reinventar la rueda y, por otra, debemos esforzarnos por hacer que el código sea reutilizable.

De hecho, la mantenibilidad del código es un tema que se debe tomar en serio porque a menudo es un equipo el que codifica y otro el que posteriormente hace el mantenimiento. También es habitual que los proyectos sean a largo plazo y estén basados en software a gran escala desarrollado durante varios años, además de que los desarrolladores solo hacen parte de la aplicación que será reutilizada por otros, con lo que cada pieza de software desarrollada se debe considerar como parte de un framework más grande y reutilizable a voluntad. Por lo tanto, el código debe ser modular, bien estructurado, documentado con javadoc (con @snippet en Java 18) y también debe poder ser verificado (y probado).

En el capítulo Documentación Spring REST Docs, veremos que también es posible documentar su código y API usando metadatos en el código, empleando por ejemplo SpringFox o anotaciones swagger.

En la historia de la informática se ha identificado un conjunto de «patrones» de diseño (design patterns) y, de este conjunto, ha surgido una variedad de buenas prácticas operativas. Se eligió una implementación de solución estándar para una serie de objetivos. Para tener claro cuáles son los objetivos de una pieza de código, es importante entender y reconocer estos patrones, con objeto de poder marcarlos. Un participante externo podrá entender inmediatamente los conceptos involucrados, el propósito, y será capaz de llegar hasta el fondo del asunto sin tener que descifrar el código.

Spring ha tomado parte de este conjunto para ofrecer soluciones sencillas a problemas que, en ocasiones, son extremadamente complejos.

Vamos a ilustrar algunos ejemplos, pero debemos tener en cuenta que Spring está orientado a design patterns (patrones de diseño) y que casi todas las partes del framework son el resultado de una reflexión simplificadora y unificadora, basado en el uso de design patterns.

Se trata de un tema complejo en sí mismo que solo veremos por encima, aunque se trata de manera extensa en el libro «Patrones de diseño en Java», escrito por Laurent Debrauwer y publicado por Ediciones ENI.

El singleton en Java Singleton

Un singleton es un objeto único en un espacio dado. Se crea la primera vez que se usa y luego se reutiliza. Lo usamos cuando queremos compartir un recurso o cuando queremos aprovechar la reentrada del código para tener solo una copia de una secuencia de procesamiento para un objetivo determinado, es decir, un método único que varios procesos pueden utilizar, ya sea de manera secuencial en un sistema de un solo thread o de manera concurrente (simultánea), gracias a la reentrada en un sistema multi thread.

1. Objetivo

Históricamente, cuando necesitábamos una acción, la implementábamos en el objeto que la requería. Por lo tanto, para un procesamiento idéntico había una multitud de copias de estas acciones en las diversas instanciaciones de los objetos que lo utilizaban. Esto se debía a que, en un sistema orientado a objetos, los procesos se encapsulan en objetos junto con los datos.

Para las acciones que no se basan en el estado del objeto que lo tiene, se decidió cambiar la acción a la parte estática de la clase. De hecho, la parte estática se comparte entre todas las instancias de la clase. Por lo tanto, no hay más duplicación de código en la memoria.

Algunos servicios se separan de los datos que procesan y, a continuación, se agrupan en clases no instanciadas como clases masivamente estáticas. Existe un sistema que, por un lado, separa los datos del procesamiento con los POJO, es decir, objetos sin procesamiento con excepción de los mecanismos de gestión de instancias (como constructores y descriptores de acceso) y, por otro lado, realiza el procesamiento separado de los datos como se haría en C. Con este tipo de arquitectura, alcanzamos rápidamente los límites del sistema.

En ese momento se desarrolla el singleton. Se trata de una clase que solo se instancia una vez y después todos la reutilizan. Soporta todos los conceptos de programación orientada a objetos. Por lo tanto, tenemos la simplicidad de uso de una clase, pero sin copias innecesarias.

images/02EP01N.png

Los primeros singletons (antes de que hubiera enumeraciones) usaban un código parecido al siguiente (en un sistema de un solo thread):

class MiSingleton 
           private static MiSingleton UNIQUE = null; 
           private MiSingleton(){ 
           } 
           public static synchronized MiSingleton getInstance() { 
         
              if (UNIQUE == null) { 
                 UNIQUE = new MiSingleton(); 
              } 
              return UNIQUE; 
           } 
        @Log 
        public class Main { 
           public static void main(String[] args) { 
              MiSingleton s1 = MiSingleton.getInstance(); 
              MiSingleton s2 = MiSingleton.getInstance(); 
         
              boolean test = s1==s2; 
              log.info("s1==s2:"+test); 
           } 

Hay una multitud de variaciones. Los más modernos usan una enumeración que, en esencia, es un singleton.

Es habitual encontrarnos con singletons en el código legacy. Cuando modificamos este código e implementamos pruebas unitarias con el framework Mockito, nos enfrentamos a las limitaciones de Mockito, que maneja mal las pruebas en partes estáticas y tenemos que usarlo junto con PowerMockito. A menudo se debe refactorizar y este refactoring en particular es costoso porque normalmente estos singletons se usan en muchos lugares del código. En sus nuevos desarrollos, ya no debe usar singletons puros y, en su lugar, debe aprovechar las funcionalidades adicionales que ofrece Singletons Spring.

2. Solución Spring

Spring permite crear Beans que sean singletons (una sola instancia) o prototipos (una instancia por bean). Cuando hablamos de una sola instancia, queremos decir que todos los objetos que tienen un bean singleton como miembro comparten este mismo objeto miembro en la memoria.

Veremos que es posible tener varios contextos Spring en una misma aplicación. Estos beans singleton definidos en la configuración Spring solo se crean una vez por contexto Spring. Por lo tanto, pueden ser múltiples en una aplicación que aloja varios contextos Spring.

Los Singletons Spring se parametrizan mediante configuración XML o anotaciones.

Cabe señalar que las anotaciones del JSR-330: Dependency Injection for Java (@Named, @Singleton) se pueden utilizar si en el classpath está presente la dependencia javax.inject. También es posible utilizar las anotaciones Spring @Bean, @Component, @Service, @Repository, etc., que especifican que usamos un singleton con peculiaridades inherentes a su función.

3. Ejemplo

En este ejemplo, se hace hincapié en que el singleton se comparte entre varios procesos, que se pueden ejecutar de manera concurrente (simultánea). Esto significa que, desde una perspectiva de memoria, puede haber dos secuencias de código que se ejecutan en paralelo en el mismo método. Uno de los procesamientos puede modificar un dato utilizado por el otro, lo que causará problemas muy rápidamente. Por lo tanto, es necesario proteger las áreas de riesgo, lo que comúnmente se llama «hacer el código Thread Safe» usando zonas sincronizadas para las que, en un momento t, solo se ejecuta un procesamiento en una porción de código, mientras que el resto de los procesos esperan su turno. Es posible proteger un proceso o variable con la palabra clave synchronized.

En general, evitaremos tener «estados» en los singletons, pero algunas veces los encontrará y deberá tener precaución. De hecho, hay efectos secundarios parasitarios en entornos multithreads.

Por ejemplo, un estado puede ser una variable de clase no estática que posteriormente se comparte por los métodos.

@Service 
        class ServicioUnico { 
           private int estado=0; 
         
           public synchronized int incEstado() { 
              //Operación larga 
              return ++estado; 
           } 
           public int getEstado() { 
              return estado; 
           } 
           public void setEstado(int estado) { 
              this.estado = estado; 
           } 
           void mostrarEstado() { 
              System.out.println(estado); 
           } 
           @Override 
           public int hashCode() { 
              final int primo = 31; 
              int resultado = 1; 
              resultado = primo * resultado + estado; 
              return resultado; 
           } 
           @Override 
           public String toString() { 
              return "ServicioUnico [estado=" + estado + "]"; 
           } 
           @Override 
           public boolean equals(Object obj) { 
              ServicioUnico other = (ServicioUnico) obj; 
              if (estado != other. estado) 
                 return false; 
              return true; 
           } 
        public class Main { 
           public static void main(String[] args) { 
                ApplicationContext context = 
        new ClassPathXmlApplicationContext("applicationContext.xml"); 
         
                ServicioUnico s1 = 
        ServicioUnico)context.getBean("servicioUnico"); 
                ServicioUnico s2 = 
        (ServicioUnico)context.getBean("servicioUnico"); 
              s1.setEstado(3); 
         
                System.out.println("Mismos valores (equals) ?: " 
        " + s1.equals(s2)); 
                System.out.println("Misma referencia (==)    ?: " 
        " + (s1==s2)); 
                System.out.println(s1); 
                System.out.println(s2); 
         
              s1.setEstado(2); 
              s2.mostrarEstado(); 
         { 
           } 

Mensajes en la consola:

¿Mismos valores (equals)?: true

Misma referencia (==) ?: true

ServicioUnico [estado=3]

ServicioUnico [estado=3]

Como veremos más adelante, Spring también permite hacer singletons en relación con un contexto de uso, utilizando scopes (ámbitos).

Por ejemplo, dado un bean de scope «session», para un usuario será único dentro de una sesión. Pero habrá tantos objetos como sesiones.

Hay otros scopes y también es posible crear sus propios scopes.

Por lo tanto, el objeto se convierte en único en su scope.

Scope

Descripción

singleton

Limita la definición del bean a una única instancia por contenedor Spring IoC (valor predeterminado). Devuelve una única instancia de bean por contenedor Spring IoC.

prototype

Limita una definición de bean único a cualquier número de instancias de objeto. Devuelve una nueva instancia de bean cada vez que se solicita.

request

Limita una definición de bean a una solicitud HTTP. Válido solo en el contexto de un Spring ApplicationContext compatible Web. Devuelve una única instancia de bean por solicitud HTTP. *

session

Extiende una definición de bean a una sesión HTTP. Válido solo en el contexto de un Spring ApplicationContext compatible Web. Devuelve una única instancia de bean por sesión HTTP. *

global-session

Extiende una definición de bean a una sesión HTTP global. Válido solo en el contexto de un Spring ApplicationContext compatible Web. Devuelve una única instancia de bean por sesión HTTP global. *

personnel

Le permite crear su propio scope.

Por ejemplo, para un Bean Spring Singleton:

@Bean 
        @Scope("singleton") 
         public Person personSingleton() { 
         return new Person(); 
        } 

Inversión de control Inversión de control

Con el principio de IOC (Inversion of Control o inversión de control), ya no se instancian las clases que implementan las funciones de una librería, sino que la aplicación las llama directamente. El programa indica que necesita una librería y se le proporciona de forma automática. Es un mecanismo externo que hace que la clase esté disponible a través de un framework o mecanismo de plugins. Por ejemplo, con Spring, la aplicación indica que necesita un servicio con una API específica y Spring le proporciona el mejor candidato. IOC

Spring permite especificar el servicio deseado, a través de una interfaz en los casos más comunes o a través de una clase cuando no hay interfaz. Para esto, generalmente utiliza una Factory de objetos que, en sí misma, es un desing pattern para hacer que el objeto esté disponible.

List<String> lista = new ArrayList<String>(); 
        UsuarioDao dao = (UsuarioDao) 
        factory.getBean("usuarioDao"); 

Una buena costumbre que se debe tener en general en Java es declarar un objeto usando la interfaz que mejor lo caracterice. En lugar de instanciar un objeto usted mismo, se deja que Spring encuentre la clase que mejor corresponda al objeto que se va a instanciar.

Se llama al mecanismo de IOC de inyección de dependencias y se utiliza la anotación @Autowired o @Inject. Más adelante veremos cómo se usan y la diferencia entre estas dos anotaciones.

Para encontrar el mejor candidato para una interfaz, Spring crea una instancia de un proxy. En sí mismo, este último es un desing pattern para objetos inyectados (que detallaremos un poco más adelante en este capítulo) y nos proporciona acceso al objeto definitivo durante el acceso.

public class LibroService { 
        @Inject 
        Private LibroDAO libroDao; 
        //implementación 
        [...] 

En tiempo de ejecución, Spring busca la clase que se corresponde con la implementación de la interfaz LibroDao (por ejemplo, LibroDaoImpl). Posteriormente, busca en su contexto un singleton que corresponda y, si no lo encuentra, lo instancia y pone su referencia en la variable LibroDao.

Este desacoplamiento permite eliminar las dependencias de los proyectos. La dependencia se convierte en dinámica y solo usamos la implementación deseada para una API determinada.

Al tomar el control de la creación del objeto, Spring también puede realizar operaciones mientras usa el objeto, agregando funcionalidades en momentos específicos, como veremos en el capítulo sobre la programación por aspectos (AOP por Aspect Oriented Programming en inglés). Entonces es posible agregar dinámicamente comportamientos a los objetos, como transaccionalidad, acceso seguro a los métodos, etc. También es posible pedir que Spring utilice un tipo de objeto diferente en un contexto particular, por ejemplo, durante la fase de prueba.

De forma predeterminada, los beans definidos en la configuración de Spring se crean solo una vez. No importa cuántas llamadas se hayan realizado; usando el método getBean() siempre habrá un único bean. De hecho, por defecto, todos los beans en Spring son singletons.

Facade Facade

Una facade es, por ejemplo, una clase que concentra las API de un conjunto de clases en un único punto de una clase de facade. Spring JDBC es un ejemplo de una facade que oculta las diferencias entre implementaciones de varias bases de datos a través de una API unificada. Spring JDBC también simplifica la gestión de excepciones relacionadas con las bases de datos. Ofrece una facade que encapsula de manera uniforme procesamientos que son muy diferentes de una base a otra, pero que se ven como idénticos en el lado de la facade.

images/cap2_pag11.png

Un servicio Spring (anotado con @Service) se puede considerar como una facade que, por un lado, expone las API de negocio y, por otro, interactúa con DAO para realizar operaciones de negocio y bases de datos.

Esta facade permite diseñar una API que puede ser sencilla y verificable. El programa que utiliza la facade ya no necesita las dependencias porque se llevan a la facade.

Factory Factory

Spring utiliza un modelo de factories para crear beans utilizando una referencia al contexto de la aplicación. Estos beans se describen en la configuración. Parte de la configuración se fija de antemano, mientras que otra parte se deduce de reglas relativamente complejas. Spring encuentra el mejor candidato para un contrato de API determinada.

BeanFactory factory = new XmlBeanFactory(new 
        FileSystemResource("spring.xml")); 
        Triangulo triangulo = (Triangulo) factory.getBean("triangulo"); 
        triangulo.draw(); 

A través de la interfaz BeanFactory, Spring proporciona una batería completa de factories para cubrir tantos casos de uso como sea posible. Esta interfaz tiene nueve subinterfaces y veinticuatro implementaciones en su última versión.

images/cap2_pag12.png

La factory puede ser visible en el caso de una aplicación standalone. Seguidamente, usamos una factory que se basa en archivos, como el mencionado en el ejemplo. Para una aplicación web, la factory se esconde en un filtro de servlet. 

Decorador

Un decorador se basa en un componente concreto que implementa una interfaz. En sí mismo, el decorador deriva de esta interfaz. Incluye el objeto original y le agrega atributos o métodos. Permite interceptar métodos, pero utiliza herencia.

images/cap2_pag13.png

Proxy Proxy

Spring utiliza mucho este desing pattern para todo lo relacionado con la programación orientada a aspectos (AOP) y el acceso remoto.

El AOP se describe en detalle en el capítulo Programación orientada a aspectos con Spring.

images/cap2_pag14.png

Spring no da acceso directo a los objetos, sino que lo hace a través de un objeto intermedio llamado proxy, que permite a Spring modificar el comportamiento del objeto original.

interface Pojo3 { 
            public void foo(); 
        } 
         
        class SimplePojo3 implements Pojo3 { 
            public void foo() { 
                System.out.println("foo"); 
            } 
        } 
         
        class RetryAdvice implements AfterReturningAdvice { 
           public void afterReturning(Object returnValue, Method method, 
                 Object[] args, Object target) throws Throwable { 
              System.out.println("Después "+method.getName()); 
           } 
        } 
         
        public class MainConAspect { 
            public static void main(String[] args) { 
                ProxyFactory factory = new ProxyFactory(new SimplePojo3()); 
                Class<?> intf=Pojo3.class; 
              factory.addInterface(intf); 
              Advice advice=new RetryAdvice(); 
                factory.addAdvice(advice); 
                factory.setExposeProxy(true); 
                Pojo3 pojo = (Pojo3) factory.getProxy(); 
                pojo.foo(); 
            } 
         
        } 

Como veremos más adelante, hay dos tipos de proxys. Para los objetos referenciados por una interfaz, usaremos un proxy Java clásico, y para los objetos que no tienen una interfaz, usaremos proxys basados en la librería CGLIB (Byte Code Generation Library o generación de byte de código de alto nivel).

El proxy permite varias cosas interesantes. Algunas veces podemos retrasar la instanciación del objeto referenciado (lazy) por el proxy. Esta instanciación solo se hace la primera vez que se accede a las propiedades del objeto, a través de una llamada al método en el proxy. Esto hace posible inyectar objetos que aún no están disponibles, pero que estarán disponibles cuando sea necesario.

Modelo Vista Controlador (MVC) MVC

Spring MVC permite simplemente crear aplicaciones web separando los tres elementos principales:

El modelo

Los datos.

La vista

Lo que se muestra.

El controlador

El procesamiento de los datos y la secuencia de vistas.

Spring delega la elección de la vista a un ViewResolver que es, en sí mismo, un desing pattern.

images/p67.png

Por ejemplo, el siguiente controlador en la solicitud de la URL /hola pedirá la visualización de la página hola.jsp y le proporcionará la fecha para que la página la muestre:

@Controller 
        public class HolaController { 
          @RequestMapping("/hola") 
          public ModelAndView hola() { 
            ModelAndView mav = new ModelAndView(); 
            mav.setViewName("hola"); 
            mav.addObject("fecha", new Date()); 
            return mav; 
          } 

Templates Templates

Las plantillas o templates se utilizan para todos los códigos genéricos.

images/cap2_pag17.png

Spring utiliza estas plantillas masivamente. Estos son algunos ejemplos:

Template

Módulo

Utilidad

RepeatTemplate

Spring Batch

Implementación de RepeatOperations

JdbcTemplate

Spring

Operación común JDBC

QueryDslJdbcTemplate

Spring Data

Soporte QueryDsl

NamedParameterJdbcTemplate

Spring

JdbcTemplate, pero con parámetros con nombre

TransactionTemplate

Spring

Gestión genérica de transacciones

HibernateTemplate

Spring

Operaciones actuales en Hibernate

JdoTemplate

Spring

JDO genérico

JpaTemplate

Spring

Operación JPA

JpaTransactionManager

Spring

Transacciones JPA

RestTemplate

Spring

Web service REST

SimpMessagingTemplate

Spring

Mensaje (por ejemplo, JMS)

Este libro presenta ejemplos para usar algunas de estas plantillas.

Las plantillas son un aspecto muy potente de Spring porque simplifican el código. Enmascaran la complejidad e incluso es posible interconectar un sistema exótico extendiendo un template Spring y ofreciendo una interfaz estándar a los desarrolladores.

Estrategia

La inyección de dependencias (DI) es un design pattern de estrategia. De hecho, siempre que desee configurar una lógica de intercambio entre beans, encontrará una interfaz que se corresponde con un constructor o un método setter apropiado en la clase de inicio para cablear su propia implementación de esta interfaz. Inyección de dependencias

images/cap2_pag19.png

Para una misma interfaz, podemos pedirle a Spring que inyecte un objeto que implemente la interfaz y, de esta manera, elegir un objeto u otro con las mismas API de procesamiento, pero con API con comportamientos diferentes. Por ejemplo, para una lista que tiene por interfaz List puede elegir entre las siguientes implementaciones: AbstractList, AbstractSequentialList, ArrayList, AttributeList, CopyOnWriteArrayList, Linked List, RoleList, RoleUnresolvedList, Stack, Vector, etc., dependiendo del comportamiento que quiera tener.

Puntos clave

  • El framework Spring está completamente orientado a desing pattern.

  • Es necesario identificar los design patterns en los comentarios del código.

  • Antes de codificar cualquier cosa, debe ver si es un desing pattern y reutilizar o especializar el código que ya implementa ese desing pattern.

  • La codificación orientada a desing pattern, permite que su código sea más fácilmente reutilizable.

Codificación equals y hashCode equals hashCode

La igualdad de objetos es importante en Spring, especialmente para las arquitecturas que usan Spring junto con un mapping objeto-relacional (en inglés Object-Relational Mapping u ORM) como Hibernate y JPA. ORM

De hecho, como detallaremos más adelante, en Spring, Hibernate y JPA hay nociones de proxy. Cuando tenemos dos objetos en memoria, hemos de tener cuidado de no comparar un proxy con el objeto al que representa.

Del mismo modo, dos objetos pueden ser idénticos desde un punto de vista de negocio, pero se corresponden con dos objetos distintos en la memoria. Veremos que este aspecto es crucial a la hora de utilizar colecciones.

Una definición según Java nos muestra lo que se espera de los métodos equals y hashCode. A partir de esta definición, veremos cómo adaptar este concepto para su uso en una arquitectura que hace uso masivo de proxys y listas, como Spring, JPA e Hibernate.

En este capítulo, usaremos «equals» para hacer referencia al método equals y «hashcode» para designar el método hashCode.

A continuación, se presenta una traducción aproximada de la documentación oficial para equals y hashCode.

equals

public boolean equals(Object obj) 

Indica si un objeto es igual al que se pasa como parámetro.

El método equals implementa una relación de equivalencia entre dos referencias de objeto no nulas.

Es reflexivo: para cualquier referencia de un valor no nulo x, x.equals(x) debe devolver true.

Es simétrico: para cualquier referencia de un valor no nulo x e y, x.equals(y) devuelve true si y solo si y.equals(x) devuelve true.

Es transitivo: para cualquier referencia de un valor no nulo x, y y z, si x.equals(y) devuelve true e y.equals(z) devuelve true, entonces x.equals(z) debe devolver true.

Es consistente: para cualquier referencia de un valor no nulo x e y, múltiples invocaciones a x.equals(y) siempre devuelven true o consistentemente false si no se modifica la información proporcionada utilizada en las comparaciones de igualdad en los objetos.

Para una referencia no nula en un valor x, x.equals(null) debe devolver false.

El método equals para la clase Object implementa la relación de equivalencia más discriminante posible en los objetos; para todos los valores de referencia no nulos x e y, este método devuelve true si y solo si x e y se refieren al mismo objeto (x == y es true).

Tenga en cuenta que, generalmente, es necesario redefinir el método hashCode siempre que el método equals esté sobrecargado, para mantener el contrato general para el método hashCode, que establece que los objetos iguales deben tener códigos hash iguales.

Argumentos

obj: la referencia del objeto con el que se va a comparar.

Valor devuelto

true si este objeto es el mismo que el que se pasa como argumento obj y false en caso contrario.

Ver también

hashCode(), HashMap.

hashCode

public int hashCode() 

Devuelve un valor de código hash para el objeto.

Este método proporciona compatibilidad con tablas hash, como las proporcionadas por HashMap.

El contrato general de hashCode es: cada vez que se llama al mismo objeto más de una vez cuando se ejecuta una aplicación Java, el método hashCode siempre debe devolver el mismo entero en tanto no se modifique la información utilizada en las comparaciones sobre el objeto.

No es necesario que este entero permanezca coherente de una ejecución de una aplicación a otra de la misma aplicación (excepto para hacerla persistente en algún momento para su reutilización).

Si dos objetos son iguales según el método equals (Object), llamar al método hashCode en cada uno de los dos objetos debe generar el mismo resultado en forma de entero.

En caso de que dos objetos no sean iguales según el método equals (java.lang.Object), el método hashCode llamado para cada uno de estos dos objetos puede generar resultados diferentes. Sin embargo, el programador debe ser consciente de que generar resultados en forma de enteros diferentes para objetos no iguales puede mejorar el rendimiento de las tablas hash.

Siempre que sea posible, el método hashCode definido por la clase Object devuelve enteros diferentes solo para objetos diferentes. Por lo general, se implementa mediante conversión de la dirección interna del objeto en un número entero, pero esta técnica de implementación no es obligatoria para el lenguaje de programación Java.

Valor devuelto

Un valor de hashcode para este objeto.

Ver también

equals(java.lang.Object),

System.identityHashCode(java.lang.Object).

1. Descripción del problema

De forma predeterminada, Java se basa en identificadores de memoria para realizar operaciones equals y hashCode. Funciona bien para casos sencillos. Sin embargo, la codificación de equals y hashcode es problemática, especialmente en entornos basados en proxy, como Spring, Hibernate y JPA.

De hecho, la columna o columnas que llevan las claves primarias y que se mapean en el objeto en la variable o variables que llevan la anotación @Id o composite (@IdClass o @EmbeddedId) se rellenan solo después de guardar en la base de datos, para columnas para las que el id no está prefijado.

Todos los sistemas basados en colecciones utilizan los métodos equals y hashcode para determinar si el elemento existe en la colección.

Tenga en cuenta que el proxy Spring en los objetos sobrecarga estos métodos. De hecho, estos métodos se deben transferir al objeto representado por el proxy, pero no se pueden interceptar a través del AOP de Spring porque ya es un objeto proxy. No puede colocar un proxy en un proxy existente.

Si el objetivo es interceptar estos métodos, será necesario utilizar AspectJ de forma nativa.

Para los proxys relacionados con lazy loading (carga diferida o bajo demanda), se debe evitar comparar las referencias y los tipos de los objetos y centrarse en el contenido, lo que fuerza la carga en el caso de JPA e Hibernate o la instanciación e inyección del objeto en Spring.

2. Implementación

La solución más razonable es utilizar una clave de negocio basada en datos reales, pero esto es bastante difícil de mantener. A menudo, utilizamos una clave primaria secuencial independiente de las claves de negocio, que se distinguen mejor por su unicidad. Para la comparación, comparamos los valores, y para el hashcode, calculamos el hashcode de las variables y combinamos estos valores para obtener un hashcode global. Las cosas se complican si hacemos que intervengan los objetos vinculados en la comparación. Esta preocupación por la comparación y el hashcode normalmente se encuentra en una parte muy genérica y repetitiva del código, con bajo valor añadido, como es la capa domaine.

En los foros (https://discourse.hibernate.org/) se ha propuesto una solución.

import java.io.Serializable; 
        import java.rmi.dgc.VMID; 
        //tomado de aquí: https://forum.hibernate.org/viewtopic.php? 
        f=1&t=928172&start=105 
        public class ModelElabore implements Serializable { 
         
            private volatile VMID vmid; 
            private Long id; 
         
            public Long getId() { 
                return id; 
            } 
            private void setId(Long id) { 
                this.id = id; 
            } 
         
            public boolean equals(Object obj) { 
                final boolean returner; 
                if (obj instanceof ModelElabore) { 
                    return getVmid().equals(((ModelElabore)obj).getVmid()); 
                } else { 
                    returner = false; 
                } 
                return returner; 
            } 
         
            public int hashCode() { 
                return getVmid().hashCode(); 
            } 
         
            private Object getVmid() { 
                if (vmid != null || vmid == null && id == null) { 
                    if (vmid == null) { //Avoid the performance impact 
                                        //of synchronized if we can 
                        synchronized(this) { 
                            if (vmid == null) 
                                vmid = new VMID(); 
                        } 
                    } 
                    return vmid; 
                } 
                return id; 
            } 

Con el uso de la clase VMID, desviamos la API de Java que permite crear un id único para todas las máquinas virtuales Java. Esta API se utiliza normalmente para administrar los garbage collectors (recolectores de basura) distribuidos con objeto de identificar las máquinas virtuales cliente.

Por lo tanto, estamos seguros de tener un identificador único. También es posible utilizar una API de la librería commons de Apache, a saber, la API org.apache.commons.id.uuid, así como la API de Java si está disponible.

Es posible que encontremos esta solución demasiado potente, pero funciona en la mayoría de los casos.

Proyecto Lombok Lombok

La librería del proyecto Lombok (https://projectlombok.org/) simplifica mucho el diseño de los POC porque minimiza el código. La mayoría de mis clientes lo usan en producción, al igual que los ejemplos de código de este libro, para reducir el tamaño del código. Es muy fácil eliminarlo y utilizar las facilidades de generación de código de su IDE (Integrated Development Environment), en general Eclipse o IntelliJ IDEA, y, si es necesario, añadir la declaración del logger.

De hecho, gracias a esta librería, podemos utilizar anotaciones que inyectan código sobre la marcha para realizar tareas comunes como los logs, descriptores de acceso, constructores y muchas otras utilidades.

Para utilizar la librería del proyecto Lombok, debe añadir la dependencia en el archivo pom.xml:

<dependency> 
           <groupId>org.projectlombok</groupId> 
           <artifactId>lombok</artifactId> 
           <version>${lombok.version}</version> 
        </dependency> 

Si usa Eclipse, debe parchear su entorno Eclipse como se indica en el sitio web del proyecto Lombok. Para hacer esto, ejecute el JAR de la librería desde la línea de comandos y seleccione su Eclipse para parchear. El archivo INI de Eclipse especifica el JAR que se cargará durante el arranque. En IntelliJ IDEA, simplemente instale el plugin Project Lombok y habilite el soporte de anotaciones processing.

Esta manipulación es necesaria para que el completado automático funcione y para que la herramienta de desarrollo no considere como un bug los métodos que no están declarados, sino que han sido inyectados por el proyecto Lombok.

Posibilidades que ofrece el proyecto Lombok en la versión «Stable»:

Anotación

Utilidad

@NonNull

Minivalidador.

@Cleanup

Cierra automáticamente los recursos como los archivos. Esta funcionalidad se incluye en Java 8 con la instrucción try con el recurso ARM (Automatic Resource Management) de Java 7 (7 y versiones superiores).

@Getter / @Setter

Agrega descriptores de acceso.

@With

Setter no mutable que crea un clon con un campo modificado.

@ToString

Crea una cadena de caracteres que contiene los campos del objeto.

@EqualsAndHashCode

Añade los métodos equals() y hashcode() (evitar con Hibernate y JPA).

@NoArgsConstructor

Añade un constructor sin argumentos.

@RequiredArgsConstructor

Añade un constructor con argumentos.

@AllArgsConstructor

Añade un constructor con todos los argumentos.

@Data

Accesos directos para @ToString, @EqualsAndHashCode, @Getter en todos los campos, @Setter en campos no finales y @RequiredArgsConstructor.

@Value

Versión inmutable de @Data. Observe que Spring también tiene una anotación @Value para inyectar valores en campos de beans administrados por Spring.

@Builder

Permite construir simplemente los builders.

@SneakyThrows

Manipulación de excepciones (se debe evitar).

@Synchronized

Simplifica las sincronizaciones.

@Getter(lazy=true)

Administra una especie de caché en un campo (se debe evitar).

@Log

Logger genérico, existe para cada implementación: @CommonsLog, @Log, @Log4j, @Log4j2, @Slf4j, @XSlf4j.

val

Al igual que con Kotlin, permite tener una variable local no mutable.

var (experimental)

Al igual que con Kotlin, permite tener una variable local mutable.

Para cada anotación, hay anotaciones secundarias. Utilizamos principalmente: @Slf4j, @Getter, @Setter, @ToString, @NoArgsConstructor y @AllArgsConstructor.

El uso de estas anotaciones ahorra mucho espacio en los ejemplos del libro. Si encuentra algún problema con su máquina, no dude en eliminar Lombok. Se puede utilizar en producción si se asegura de que todo se haya probado en la plataforma de destino de antemano. Anotaciones

Sistemas de registro de actividad o log Log

Log4j, SLF4J y Logback Log4j SLF4J Logback

Durante muchos años, se ha utilizado el sistema de registro Log4j de manera tradicional para registrar mensajes de información, depuración y error. Sin embargo, la nueva librería Logback (http://logback.qos.ch/) es mucho más potente que Log4j.

Con Log4j, debe prestar atención a los problemas de seguridad que se detectaron en la versión anterior a la 2.17.

Históricamente, JCL (Jakarta Common Logging) fue una de las primeras API concurrentes, pero siguió siendo muy limitada. Alrededor de 2006, grandes proyectos como Hibernate adoptaron la API SLF4J (http://www.slf4j.org/), que la popularizó. Sin embargo, a pesar de un buen comienzo, SLF4J no eclipsó totalmente a Log4j.

Sin embargo, gracias a SLF4J, es posible centralizar en un único conjunto de archivos de registro las llamadas que realizan las diferentes API de registro, como JCL, Log4j, JUL (http://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html), etc.

En la actualidad, se agrupan los registros de las llamadas a las API de log y es posible elegir qué implementación de registro utilizar. A diferencia de JCL, que carga las librerías en tiempo de ejecución con el classloader, SLF4J elige la implementación de destino del paquete correspondiente al puente de software entre los tipos de API.

images/cap3_pag13.png

La API Logback de sesión es una de las mejores API de registro en la actualidad. Ofrece mucha flexibilidad para hacer logs. También es posible asociar Logback con SLF4J y Lombok:

package fr.eni; 
        import lombok.extern.slf4j.Slf4j; 
        import org.slf4j.Logger; 
        import org.slf4j.LoggerFactory; 
        @Slf4j 
        public class LosLogs { 
           private static final Logger slf4jLogger = LoggerFactory 
                 .getLogger(LesLogs.class); 
           public static void main(String[] args) { 
              slf4jLogger.trace("Hola a todos"); 
              String nombre = "John CONNOR"; 
              slf4jLogger.debug("Hi, {}", nombre); 
              slf4jLogger.info("Log de tipo info."); 
              slf4jLogger.warn("Log de tipo warn."); 
              slf4jLogger.error("Log de tipo error."); 
              log.info("Log de tipo info a través de lombok, {}", nombre); 
         
           } 

Siempre que sea posible, en este libro se utilizan los logs Logback.

Nos hemos tenido que poner límites para este capítulo, pero Logback funciona muy bien en una stack ELK (Elasticsearch-Logstash-Kibana). Siempre que sea posible, tenga en cuenta que nuestros logs se pueden utilizar para ayudar a las personas encargadas de la explotación y la producción de nuestras aplicaciones en los entornos de destino. Por lo tanto, será necesario registrar el máximo de datos utilizables. Algunos usuarios no habilitan los registros de actividad de nivel Info en producción para solucionar los problemas relacionados con el rendimiento, con lo que posteriormente, en los registros de tipo Error, será necesario recordar todo el contexto de la llamada.

Bases de datos H2 H2

1. Descripción del problema

Hoy en día, hay muchas bases de datos relacionales en el mundo SQL que se utilizan en una amplia variedad de contextos. Las versiones comerciales más populares son Oracle, DB2, SQL Server y Sybase. Existen otras bases de datos gratuitas, como Derby, Firebird, HSQLDB, H2, Ingres, MariaDB, MySQL y PostgreSQL, por nombrar solo las más conocidas. Bases de datos

Normalmente, vamos a utilizar HSQLDB y H2 por sus amplias posibilidades de uso. Esto nos permite tener la base de datos SQL más sencilla posible para hacer una aplicación ligera y realizar pruebas. Estas bases de datos se pueden ejecutar en memoria o con archivos, ofrecen consolas para realizar operaciones en ellas y son muy ligeras en términos de ocupación de memoria o espacio en disco.

Vamos a trabajar preferiblemente con H2, que aporta algunas ventajas valiosas, como el soporte de compatibilidad SQL con Oracle, DB2 y MySQL. Spring ha utilizado HSQLB nativo desde las primeras versiones para ayudarnos a hacer nuestras pruebas unitarias, pero veremos cómo usar H2 en su lugar.

2. Implementación

a. Instalación

Descargue la base de datos desde la dirección http://www.h2database.com/ e instálela.

La consola de texto o web están disponible a través del enlace en el menú Inicio o desde la línea de comandos.

La documentación en el sitio web de distribución es muy completa.

b. Configurar el POM POM:configuración

Para poder usar H2 en un proyecto mavenizado, debe añadir la siguiente dependencia en el archivo pom.xml.

<dependency> 
           <groupId>com.h2database</groupId> 
           <artifactId>h2</artifactId> 
           <version>2.1.212</version> 
        </dependency> 

Las versiones evolucionan con frecuencia. Es necesario utilizar una versión reciente porque las versiones anteriores no gestionan una parte de los códigos de error de SQL.

c. Conexión a la base de datos mediante JDBC JDBC

Podemos conectarnos a la base de datos desde el código Java a través del siguiente código:

public class ConnectH2 { 
           public static void main(String... args) throws Exception {  
              //Eliminamos la base de datos 
              DeleteDbFiles.execute("~", "test", true);  
              //Conexión 
              Class.forName("org.h2.Driver"); 
              Connection connection = DriverManager.getConnection("jdbc:h2:~/ 
        test"); 
              Statement statement = connection.createStatement();  
              //Creamos una tabla 
              statement.execute("create table test(id int primary key, 
        name varchar(255))");  
              //Insertamos un registro 
              statement.execute("insert into test values(1, 'Ediciones ENI')"); 
              //Leemos el registro 
              ResultSet rs; 
              rs = statement.executeQuery("select * from test"); 
              while (rs.next()) {  
              //Lo mostramos 
               System.out.println(rs.getString("name")); 
              }  
              //Close Statement 
              statement.close();  
              //Cierre de la base de datos 
              connection.close(); 
           } 

En este ejemplo de código, comenzamos borrando las trazas que pudieran quedar del último uso de esta base de datos usando el comando DeleteDbFiles.execute, pero, en general, no tendremos que hacer esta operación.

Luego cargamos el driver y creamos una conexión.

A continuación, creamos una tabla e insertamos una fila o registro que seguidamente leemos para mostrarla.

Luego, cerramos la conexión a la base de datos.

d. Usar un listener de servlets para iniciar y detener la base de datos Listener

Podemos configurar el archivo web.xml del proyecto web para gestionar la base de datos:

<web-app> 
           <display-name>Archetype Created Web Application</display-name> 
           <context-param> 
                 <param-name>db.url</param-name> 
                 <param-value>jdbc:h2:~/test</param-value> 
           </context-param> 
           <context-param> 
                 <param-name>db.user</param-name> 
                 <param-value>sa</param-value> 
           </context-param> 
           <context-param> 
                 <param-name>db.password</param-name> 
                 <param-value></param-value> 
           </context-param> 
           <context-param> 
                 <param-name>db.tcpServer</param-name> 
                 <param-value>-tcpAllowOthers</param-value> 
           </context-param> 
           <listener> 
                 <listener-class>org.h2.server.web.DbStarter</listener-class> 
           </listener> 
        </web-app> 

Esto hace que sea fácil tener una base de datos en un sitio web Java.

Hacemos poco uso de esta posibilidad porque normalmente dejaremos la gestión de la base de datos a Hibernate o JPA. Del mismo modo, como veremos en el capítulo sobre las pruebas, las librerías de pruebas unitarias también soportan la gestión de la base de datos H2.

Proyectos Maven Maven

El framework Java ofrece un JDK básico lleno de API, lo que facilita la creación de librerías y su uso compartido. Han aparecido muchas librerías.

Estas librerías usan otras librerías y, al final, terminamos con dependencias. Las cosas se complican cuando, por ejemplo, tenemos dos librerías que usan otras dos librerías, pero en diferentes versiones. Entonces, el juego consiste en encontrar la combinación correcta. El orden de carga también es muy importante, porque en los servidores JEE las librerías del servidor tienen prioridad sobre las librerías de la aplicación.

Por supuesto, es posible invertir la prioridad cargando primero las librerías de aplicación antes que las del servidor, pero entonces perdemos el soporte del proveedor del servidor de aplicaciones. Por lo general, tenemos un directorio para las librerías del servidor y otro para las librerías de cada proyecto.

1. Descripción del problema

Más allá de un determinado número de proyectos, la gestión de dependencias se vuelve rápidamente esencial. Muchas herramientas de gestión de dependencias intentan abordar estos problemas. Inicialmente, usamos makefiles que fueron reemplazados por Ant (http://ant.apache.org/: un makefile evolucionado), que, a su vez, fue reemplazado por Maven. Gradle (https://gradle.org/) también se utiliza, pero es más apropiado para proyectos de Android o proyectos que requieren delicadeza a nivel de las acciones que se deben realizar antes, durante y después de las fases de build, test y packaging. A pesar de que el framework Spring utiliza Gradle para administrar sus dependencias de manera interna, usaremos Maven en los ejemplos del libro en aras de la simplificación. Gradle

2. Implementación

Maven es compatible entre Windows, Mac y Unix (Linux). De la misma manera que un makefile, permite generar archivos binarios y, de paso, empaquetarlos a partir de un conjunto de archivos fuente. Utiliza un concepto llamado POM (Project Object Model), que describe un proyecto modular y los pasos necesarios para compilarlo, probarlo, empaquetarlo, instalarlo y desplegarlo. POM

Maven centraliza en espacios de almacenamiento compartidos, llamados «repositorios públicos» en Internet, la mayoría de las versiones de proyectos open source que son dependientes. A través de un repositorio local, pone a disposición de nuestro proyecto una copia de los archivos necesarios a partir del nombre de la dependencia y de su versión, e indica al proyecto dónde encontrar sus dependencias en el repositorio local.

También es posible guardar las versiones de sus propios módulos en el repositorio local para compartirlas entre nuestros diferentes proyectos.

Los proyectos y subproyectos se configuran a través del archivo pom.xml, que se encuentra en el directorio raíz del proyecto. Este archivo describe dependencias e información sobre el proyecto. Es posible hacer una jerarquía de configuración POM donde las propiedades de los POM padres en la jerarquía se sobrecargan en los POM hijos. También es posible externalizar determinados elementos relacionados con la configuración para poder personalizarla en función de los entornos de ejecución de la aplicación.

Con el fin de simplificar el tema, solo veremos el uso simplificado para ilustrar las posibilidades de Spring. No utilizaremos el mecanismo de los POM padres para dar visibilidad y facilidad de compilación y prueba de ejemplos.

3. Instalación de Maven en Windows Maven:instalación

En esta sección se describe la instalación en Windows. Para otras plataformas, siga las recomendaciones de instalación para su sistema operativo en la página que describe la instalación de Maven.

 Descargue e instale Maven como se indica en el sitio web de Maven Apache, en la dirección http://maven.apache.org/download.cgi

 Edite el archivo de configuración de Maven settings.xml si desea cambiar la ubicación del repositorio local. De forma predeterminada, Maven coloca el repositorio en un directorio .m2 en el directorio correspondiente a su usuario. Algunas veces, debe apuntar a un espacio en disco más grande porque tiende a crecer rápidamente.

 Apunte las variables M2_HOME y M3_HOME al directorio de instalación de Maven.

 Cambie el PATH para que apunte a %M3_HOME%\bin.

 Para configurar cómo utiliza la memoria Maven, sitúe en MAVEN_OPTS los argumentos JVM; por ejemplo, -Xms256m -Xmx512m para Java 7 y MAVEN_OPTS="-Xmx1g -XX:MaxPermSize=400m" para Java 8. También debe hacer que JAVA_HOME apunte al JDK.

 Abra una ventana de DOS o Git Bash para comprobar que el comando Maven responde correctamente: mvn --version.

Como hemos mencionado, Maven utiliza un archivo pom.xml para la configuración del paquete que describe las dependencias y cómo compilar, probar y empaquetar la aplicación.

Veremos el contenido de este archivo un poco más adelante.

4. Utilizar un arquetipo Maven

Maven también se puede utilizar para crear un esqueleto de aplicación de forma muy sencilla.

Por ejemplo, podemos crear un proyecto simple con el siguiente comando:

mvn archetype:generate -DgroupId=simple -DartifactId=simple 
        -DpackageName=fr.eni -Dversion=1.0-SNAPSHOT 

En este caso, Maven está en modo interactivo y hace algunas preguntas a las que es suficiente con responder validando el valor predeterminado.

Posteriormente, Maven crea un proyecto «hello world» con el archivo pom.xml asociado.

El siguiente comando lo compila e inicia el programa:

$ mvn exec:java -Dexec.mainClass="simple.App" 
        [INFO] Scanning for projects... 
        [INFO] 
        [INFO] ---------------------------------------------------------- 
        [INFO] Building cap03-simple 1.0-SNAPSHOT 
        [INFO] ---------------------------------------------------------- 
        [INFO] 
        [INFO] --- exec-maven-plugin:1.4.0:java (default-cli) @ cap04-simple --- 
        Hello World! 
        [INFO] ---------------------------------------------------------- 
        [INFO] BUILD SUCCESS 
        [INFO] ---------------------------------------------------------- 
        [INFO] Total time: 6.420 s 
        [INFO] Finished at: 2015-07-21T13:31:25+02:00 
        [INFO] Final Memory: 10M/309M 
        [INFO] ---------------------------------------------------------- 

El comando mvn site genera una documentación completa del proyecto Maven en el directorio target/site.

Otra posibilidad para generar el POM de un proyecto Spring consiste en utilizar el servicio Spring Initializr, en la dirección https://start.spring.io/

5. Contenido del archivo pom.xml en los casos sencillos utilizados en este libro pom.xml

He aquí el contenido del archivo pom.xml de la sencilla aplicación que acabamos de generar, ajustado con alguna información adicional:

<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>simple</groupId> 
          <artifactId>chap05-simple</artifactId> 
          <version>1.0-SNAPSHOT</version> 
          <packaging>jar</packaging> 
          <name>chap04-simple</name> 
          <url>http://maven.apache.org</url> 
           <properties>  
              <java-version>1.8</java-version> 
              <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
              <org.springframework-version>4.3.14-RELEASE 
        </org.springframework-version> 
              <spring.version>4.3.30-RELEASE</spring.version> 
           </properties> 
          <dependencies> 
            <dependency> 
              <groupId>junit</groupId> 
              <artifactId>junit</artifactId> 
              <version>4.13.2</version> 
              <scope>test</scope> 
            </dependency> 
          </dependencies> 
        </project> 
           <build> 
              <finalName>${project.artifactId}</finalName> 
              <plugins> 
                 <plugin> 
                    <groupId>org.apache.maven.plugins</groupId> 
                    <artifactId>maven-compiler-plugin</artifactId> 
                    <version>3.8.1</version> 
                    <configuration> 
                       <source>${java-version}</source> 
                       <target>${java-version}</target> 
                    </configuration> 
                 </plugin> 
                 <plugin> 
                    <groupId>org.apache.maven.plugins</groupId> 
                    <artifactId>maven-surefire-plugin</artifactId> 
                    <version>2.22.2</version> 
                    <configuration> 
                       <includes> 
                          <include>**/*Tests.java</include> 
                       </includes> 
                       <excludes> 
                          <exclude>**/Abstract*.java</exclude> 
                       </excludes> 
                       <junitArtifactName>junit:junit</junitArtifactName> 
                       <argLine>-Xmx512m</argLine> 
                    </configuration> 
                 </plugin> 
                 <plugin> 
                    <groupId>org.apache.maven.plugins</groupId> 
                    <artifactId>maven-dependency-plugin</artifactId> 
                    <executions> 
                       <execution> 
                          <id>install</id> 
                          <phase>install</phase> 
                          <goals> 
                             <goal>sources</goal> 
                          </goals> 
                       </execution> 
                    </executions> 
                 </plugin> 
              </plugins> 
           </build>  

En relación con el proyecto generado, añadimos el hecho de que queremos utilizar Java 8 y UTF-8 (en negrita en el código anterior). También indicamos que usamos Spring en el proyecto.

Spring y las versiones de Java

Las versiones de Spring siempre han sido compatibles con las versiones de Java que están activas en el momento de la publicación de la versión. Algunas veces, Spring estaba ligeramente por delante.

Java

Año

Versión

Fin de vida

1995

JDK Beta

...

...

Febrero de 2002

J2SE 1.4

Octubre de 2008

Septiembre de 2004

Java SE 5

Noviembre de 2009

Diciembre de 2006

Java SE 6

Abril de 2013

Julio de 2011

Java SE 7

Julio de 2019

Marzo de 2014

Java SE 8 (LTS)

Diciembre de 2030

Septiembre de 2017

Java SE 9

Marzo de 2018

Marzo de 2018

Java SE 10

Septiembre de 2018

Septiembre de 2018

Java SE 11 (LTS)

Septiembre de 2026

Marzo de 2019

Java SE12

Septiembre de 2019

Septiembre de 2019

Java SE 13

Marzo de 2020

Marzo de 2020

Java SE 14

Septiembre de 2020

Septiembre de 2020

Java SE 15

Marzo de 2021

Marzo de 2021

Java SE 16

Septiembre de 2021

Septiembre de 2021

Java SE 17 (LTS)

Septiembre de 2029

Marzo de 2022

Java SE 18

Septiembre de 2022

Septiembre de 2022

Java SE 19

Marzo de 2023

Marzo de 2023

Java SE 20

Septiembre de 2023

Septiembre de 2023

Java SE 21 (LTS)

Septiembre de 2028

Spring

Año

Versión

2003

1.0

2006

1.0

2009

3.0

Enero de 2019

3.2

Diciembre de 2020

4.3

9 de diciembre de 2020

5.0

2020

5.3

2017

5.0

2020

5.3

En el GitHub de Spring, para las versiones soportadas se especifica:

La versión 5.3.x es la última feature branch de la 5.ª generación y la última línea de producto (GA a partir de octubre de 2020), con soporte a largo plazo proporcionado para JDK 8, JDK 11 y JDK 17.

La versión 5.2.x es la línea de producto anterior (GA a partir de septiembre de 2019), que se soportará de manera activa hasta finales de 2021.

Las versiones 5.1.x y 5.0.x ya no son compatibles de manera activa, sino que se sustituyen por 5.2.x y 5.3.x a partir de diciembre de 2020.

La versión 4.3.x alcanzó el final de su vida oficial (EOL) el 31 de diciembre de 2020. No se prevén otros parches de mantenimiento o seguridad en esta línea.

La versión 3.2.x alcanzó el final de su vida oficial (EOL) el 31 de diciembre de 2016. No se prevén otros parches de mantenimiento o seguridad en esta línea.

En este punto, Spring recomienda que actualicemos a la última versión de Spring Framework 5.3.x desde Maven Central.

Gama de versiones del JDK:

Spring Framework 6.0.x: JDK 17-21 (esperado)

Spring Framework 5.3.x: JDK 8-19 (esperado)

Spring Framework 5.2.x: JDK 8-15

Spring Framework 5.1.x: JDK 8-12

Spring Framework 5.0.x: JDK 8-10

Spring Framework 4.3.x: JDK 6-8

Spring está probado y es totalmente compatible con las versiones Spring on Long-Term Support (LTS) del JDK, es decir, actualmente JDK 8, JDK 11 y JDK 17. Además, hay soporte para versiones intermedias, como JDK 9/10/12/13/14/15/16/18 siempre que sea posible, lo que significa que Spring acepta informes de bugs e intenta solucionarlos técnicamente, pero no proporciona ninguna garantía de nivel de servicio.

Spring Boot

Año

Versión

2014

1.0

2014

1.1

2014-2015

1.2

2015-2016

1.3

2017

1.5

Marzo de 2018

2.0

Octubre de 2018

2.1.x

Mayo de 2020

2.3.x

Diciembre de 2020

2.4.x

Mayo de 2021

2.5.x

Noviembre de 2021

2.6.x

En el GitHub de Spring Boot, se especifica:

Spring Boot sigue la política de soporte de VMware Tanzu OSS para bugs críticos y problemas de seguridad.

Las versiones principales se soportan durante, al menos, 3 años a partir de la fecha de lanzamiento (pero debe ejecutar una versión menor soportada). Las versiones menores son compatibles durante, al menos, 12 meses. El soporte comercial también está disponible en VMware, que ofrece un período de soporte extendido.

Todas las versiones de Spring Boot están disponibles públicamente en Maven Central y en la dirección https://repo.spring.io. No hay un repositorio privado reservado solo para clientes de pago.

Fin de la vida útil

Las versiones de Spring Boot se marcan como «Fin de vida útil» cuando ya no son compatibles o publicadas de ninguna manera. Si está utilizando una versión de EOL, debe actualizarse lo antes posible.

Las versiones de Spring Boot generalmente se marcan como final de su vida útil 27 meses después del lanzamiento.

Es posible que una versión ya no sea compatible incluso antes de su fin de vida útil. Durante este período, solo debemos esperar a las versiones para bugs críticos o problemas de seguridad.

Esto implica varias cosas: necesitamos estar al día a nivel del JDK, Spring y Spring Boot para no acumular deuda técnica y mantenernos actualizados a nivel de la resolución de los problemas de seguridad, que están creciendo en número.

Si tiene que intervenir en el código antiguo, debe intentar migrarlo a las últimas versiones. Si esto no es posible, los componentes se deben actualizar tanto como sea posible en su versión dentro de su contexto, comprobando que estas versiones no plantean problemas de seguridad.

Puntos clave

  • La versión 5 del framework Spring requiere al menos Java 8.

  • Los métodos equals y hashCode se deben adaptar para su uso con Spring, Hibernate y JPA.

  • En los ejemplos, usamos el proyecto Lombok para ahorrar código.

  • Usamos Logback para tener buenos registros de actividad o logs.

  • Se mantiene la base de datos H2 en los ejemplos por su facilidad de uso.

  • Es necesario seguir las actualizaciones de versión para minimizar la deuda técnica y contrarrestar las brechas de seguridad.

  • En los ejemplos, usamos Maven en su forma más sencilla.

Introducción

Este capítulo presenta un uso simplificado de Spring, para que tengamos una visión general, sin perdernos en los detalles. Posteriormente, veremos aclaraciones sobre las partes que es necesario conocer para afrontar una aplicación más compleja. El ejemplo que lo ilustra ya permite experimentar con una gran parte de los problemas relacionados con el uso de este framework.

VM6883:64

Origen

Vamos a bordar los principales componentes de Spring.

Spring es un framework que simplifica la programación. Se compone de un núcleo, Spring Core, que permite una administración sencilla de instancias de clase en memoria y librerías de clases que utilizan este núcleo.

A estas clases se las llama beans Spring. El framework proporciona un conjunto de beans preprogramados que cubren un espectro muy amplio de casos de uso, que encontramos cuando codificamos una aplicación compleja.

Son ampliables y fáciles de usar.

El núcleo del framework permite cargar un conjunto de singletons durante el arranque y facilita su acceso inyectando automáticamente su referencia (ubicación en memoria) en los objetos que los usan.

Spring permite tener un control muy fino sobre la gestión de objetos en memoria. 

Spring interactúa con muchos frameworks y otros productos. El principal interés de Spring es la instanciación y la disponibilidad automatizada de beans. Estos objetos pueden ser de dos tipos: singletons, como ya hemos mencionado, así como objetos «duplicados» llamados «prototipos».

A diferencia de lo que sucede con el objeto Prototype, el objeto Singleton es un objeto compartido del que se crea una única instancia en un mismo contenedor Spring y cuyo uso es compartido. Spring gestiona internamente una lista de singletons instanciados. Si se debe inyectar un bean que es miembro de otro bean y ya está cargado en la memoria, Spring copia su referencia. De lo contrario, lo instanciará y lo pondrá en su lista. A continuación, lo inyecta de forma automática. Normalmente, Spring crea los singletons cuando se inicia el contenedor Spring, durante el arranque de la aplicación. Hay algunas excepciones a esta regla, que veremos más adelante.

Un objeto Prototype es un objeto del que se crea una instancia cada vez que Spring lo inyecta en el miembro de un objeto Spring. Todos los mecanismos de facilitación que ofrece Spring están disponibles para objetos Prototype, pero el uso de este tipo de objetos es relativamente raro.

El interés del bean prototype es beneficiarse de todas las ventajas de Spring para los objetos que no son singletons.

Veremos que Spring también permite controlar el ciclo de vida (creación, destrucción, etc.) y ofrece la posibilidad de interceptar las llamadas de los métodos de los objetos gestionados para poder tomar el control de los estos usando la programación orientada a aspectos que se integra.

Veremos que hay cuatro maneras de configurar los beans. Estas tipologías de configuración son intercambiables y es posible mezclarlas: la configuración puede ser implícita o explícita. En el primer caso, Spring descubre los beans cuando se inicia el contenedor recorriendo las clases. En el segundo caso, Spring solo tiene en cuenta la ubicación y la función exacta de los beans así declarados.

Spring facilita la integración de la aplicación con su ecosistema y la llamada a los servicios web estándares o Hessian, Burlap, Rmi, RPC entre otros. Spring también permite el uso de EJB.

Los módulos fundamentales

Los beans especializados que se muestran a continuación generalmente están presentes desde la versión 0.9 de Spring y han recibido mejoras a lo largo de las versiones. Se utilizan sobre todo cuando queremos extender Spring. Los verá principalmente en los frameworks. Cuando usamos anotaciones, dejamos de ver estos objetos, pero Spring los utiliza internamente.

1. Composición de un bean

Veremos más en detalle cómo funciona un bean en la programación orientada a aspectos con Spring dedicado a AOP.

Para simplificar, un bean se puede considerar como un proxy que extiende un objeto Java. El proxy permite:

  • capturar las llamadas a los métodos del objeto para añadir comportamientos,

  • añadir nuevos métodos,

  • gestionar vínculos a los objetos a los que se hace referencia en el objeto principal,

  • posibilidad de asignar valor a las propiedades del objeto de diferentes maneras: cadenas, archivos (a través de una factory),

  • gestionar mensajes en los bundles (a través del contexto),

  • gestionar eventos entre objetos: creación, destrucción o eventos de usuario.

El bean está orientado a datos (POJO) o procesamientos, intentando separar estos aspectos en beans especializados.

Por lo tanto, contiene:

  • la clase de implementación real del bean,

  • elementos de configuración conductual del bean, singleton/prototype,

  • beans relacionados con la inyección de dependencias, etc.,

  • valores de propiedad que se establecerán durante la construcción.

Cuando queremos usar un bean Spring, añadimos un miembro de clase e indicamos a Spring que queremos utilizarlo escribiendo la variable con la interfaz correspondiente al Bean. Spring tiene diferentes estrategias para encontrar el Bean adecuado que se va a inyectar.

Un bean se identifica por su nombre y su identificador. Los identificadores pueden tener alias, pero este uso es bastante confuso. Varios beans pueden tener el mismo identificador, por lo que se indicará a Spring cuál debe tener prioridad (@Primary).

2. El singleton y el prototipo

Un singleton, que es el tipo predeterminado de beans, solo tiene una instancia de implementación dentro de un contexto, mientras que el prototipo puede tener varias.

3. Los objetos fundamentales del paquete core

Este paquete gestiona la inyección de dependencias. Utiliza un determinado número de clases, que hay que conocer.

a. El PropertyEditor

Spring utiliza los editores de propiedades para gestionar la conversión entre valores String y tipos personalizados Object.

Hay un enlace automático y otro personalizado.

La infraestructura JavaBeans estándar detecta automáticamente los objetos PropertyEditor si están en el mismo paquete que la clase que administran.

Estas clases PropertyEditor deben tener el mismo nombre que la clase, con el sufijo Editor.

Spring los reutiliza masivamente para inicializar sus beans a partir de los archivos XML. También es ampliamente utilizado por Spring MVC.

Vinculación automática

Hay una detección automática si la clase del bean y el objeto Property-Editor representado por la clase con el sufijo Editor están en el mismo paquete. 

Si las clases Ejemplo.java y EjemploEditor.java se encuentran en el mismo paquete:

@Data 
        @ToString 
        public class Ejemplo { 
               private String cadena; 
               private Integer entero; 
        } 
         
        public class EjemploEditor extends PropertyEditorSupport { 
            @Override 
            public String getAsText() { 
               Ejemplo ejemplo = (Ejemplo) getValue(); 
                return ejemplo == null ? "" : 
        ejemplo.getCadena()+">"+ejemplo.getEntero(); 
            } 
         
            @Override 
            public void setAsText(String text) throws 
                          IllegalArgumentException { 
                if (StringUtils.isEmpty(text)) { 
                    setValue(null); 
                } else { 
                     Ejemplo ejemplo = new Ejemplo(); 
                   StringTokenizer st = new StringTokenizer(text,">"); 
                   ejemplo.setCadena(st.nextToken()); 
                   ejemplo.setEntero(Integer.parseInt(st.nextToken())); 
                    setValue(ejemplo); 
                } 
            } 
        } 

Con un controlador:

@Slf4j 
        @Controller 
        public class MiRestController { 
         
               @GetMapping(value = "/test/{id}") 
               public @ResponseBody String test1(@PathVariable String id) { 
                     return id; 
               } 
         
               @GetMapping(value = "/test2/{ex}") 
               public @ResponseBody String test2(@PathVariable Ejemplo ex) { 
                     String ret=ex.toString(); 
         
                     return ret; 
               } 
        } 

En una llamada:

http://localhost:8080/test2/aaa%3E1 

Tenemos:

Ejemplo(cadena=aaa, entero=1) llamada: 

Vinculación personalizada

Si el binder está en un paquete diferente al de la clase, debe inicializar el binder en el controlador:

@Slf4j 
        @Controller 
        public class MiRestController { 
         
               @GetMapping(value = "/test3/{ex}") 
               public @ResponseBody String test3(@PathVariable Ejemplo2 ex) { 
                     String ret=ex.toString(); 
         
                     return ret; 
               } 
         
               @InitBinder 
               public void initBinder(WebDataBinder binder) { 
                   binder.registerCustomEditor(Ejemplo2.class, 
                       new CustomEditorEditor()); 
               } 
        } 

4. PropertyValues

Un PropertyValues contiene varios PropertyValue.

El PropertyValue de un objeto contiene la información y el valor de una propiedad de bean individual. Entre otras cosas, permite gestionar propiedades indexadas de forma optimizada. Se utiliza en los convertidores que veremos en el capítulo Configuración avanzada. También permite especificar si el valor se debe ignorar cuando no existe en el objeto de destino.

Tenga en cuenta que no es necesario que el valor sea de un tipo predefinido porque una implementación BeanWrapper debe ser capaz de gestionar todas las conversiones necesarias. De hecho, este objeto no sabe nada de los objetos a los que se unirá.

5. BranWrapper

BranWrapper es una clase cuyas instancias encapsulan un bean. Permite acceder a las propiedades y manipular un bean.

Por ejemplo, como se especifica en los JavaBeans, podemos acceder a una propiedad usando su nombre en una matriz que indexa las variables de un bean.

Trataremos este objeto con más detalle en el capítulo Configuración avanzada.

6. BeanFactory

Esta factory permite crear un bean desde cero a partir de una interfaz o clase, así como desacoplar la configuración y las dependencias del código funcional del objeto.

Esta factory tiene derivados, como XmlBeanFactory, que consigue su definición de un archivo XML.

Una BeanFactory gestiona la definición de un bean:

  • su naturaleza (Singleton o Prototype),

  • sus propiedades,

  • los métodos a los que se va a llamar durante la inicialización y la destrucción,

  • el cableado del bean a otros beans.

BeanFactory rara vez se usa. Es preferible Application-Context, que se analiza a continuación, porque es más versátil.

Para cargar un bean, cargamos su definición usando un archivo XML, por ejemplo, y seguidamente lo recuperamos con el método getBean().

7. La interfaz BeanDefinition

Un BeanDefinition describe una instancia de bean:

  • valores de propiedad,

  • valores de argumento de constructor,

  • otra información proporcionada por implementaciones concretas.

Se trata de una interfaz mínima que permite a un BeanFactoryPostProcessor hacer introspección y modificar los valores de propiedad y otros metadatos del bean a medida que la BeanFactory lo crea.

Hablaremos más en detalle sobre BeanFactoryPostProcessor más adelante.

La BeanFactory se ocupa de la inyección de dependencias. Utiliza una BeanDefinition y los PropertyEditors con los PropertyValue para especificar los valores de los atributos.

La inyección de dependencias se puede hacer indicando en una propiedad o en su setter, preferentemente, que se trata de una dependencia a través de los argumentos del constructor, para lo cual indicamos que queremos una inyección de dependencias.

8. PropertyPlaceholderConfigure

Permite externalizar propiedades desde un archivo que contiene las definiciones de los beans. Un elemento de configuración se devuelve a un archivo externo.

PropertyPlaceholderConfigure está en desuso desde Spring 5.2 y se sustituye por PropertySourcesPlaceholderConfigurer.

PropertyPlaceholderConfigure en XML

Archivo de configuración XML para una aplicación con una base de datos a la que se accede a través de JDBC:

<?xml version="1.0" encoding="UTF-8"?> 
        <beans xmlns="http://www.springframework.org/schema/beans" 
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
          xsi:schemaLocation="http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans.xsd"> 
          <bean class="org.springframework.beans.factory.config.Property 
        PlaceholderConfigurer"> 
          <property name="locations" value="classpath:jdbc.properties" /> 
          </bean> 
          <bean id="dtSource" destroy-method="close" 
        class="org.apache.commons.dbcp.BasicDataSource"> 
          <property name="driverClassName" value="${jdbc.driverClassName}" /> 
          <property name="url" value="${jdbc.url}" /> 
          <property name="username" value="${jdbc.username}" /> 
          <property name="password" value="${jdbc.password}" /> 
          </bean> 
          <bean id="compteDAO" class="fr.eni.CompteDAO"> 
          <constructor-arg name="dataSource" ref="dtSource"/> 
          </bean> 
        </beans> 

El archivo de configuración de JDBC:

jdbc.properties 
        jdbc.driverClassName=com.mysql.jdbc.Driver 
        jdbc.url=jdbc:mysql://localhost:3306/mabase 
        jdbc.username=root 
        jdbc.password= 

PropertySourcesPlaceholderConfigure en Java

@Configuration  
        @PropertySource("classpath:jdbc.properties"public class AppConfig { 
           @Value("${jdbc.driverClassName}") 
           private String driverClassName; 
           @Value("${jdbc.url}") 
           private String jdbcURL; 
           @Value("${jdbc.username}") 
           private String username; 
           @Value("${jdbc.password}") 
           private String password; 
           @Bean 
           public DataSource getDataSource() { 
          BasicDataSource dataSource = new BasicDataSource(); 
          dataSource.setDriverClassName(driverClassName); 
          dataSource.setUrl(jdbcURL); 
          dataSource.setUsername(username); 
          dataSource.setPassword(password); 
          return dataSource; 
           } 
           @Bean 
           public CompteDAO compteDAO(DataSource dataSource) { 
          return new   CompteDAO(dataSource); 
           } 
           @Bean 
           public static PropertySourcesPlaceholderConfigurer 
        placeHolderConfigurer() { 
          return new PropertySourcesPlaceholderConfigurer(); 
           } 
        } 

Aquí vemos el uso de SpEL (Spring Expression Language):

@Value("${jdbc.password}") 

Indica que debe buscar el valor jdbc.password en la configuración.

9. Los objetos fundamentales del paquete context

ApplicationContext desempeña el mismo papel que un registro JNDI en un servidor Jakarta EE que permite la gestión y manipulación de beans.

Ofrece todas las funcionalidades de un BeanFactory y añade otras.

En resumen, un contexto contiene una agrupación de beans.

Existe el contexto simple y sus derivados:

images/cap4_pag12.png

Se basa en BeanFactory y añade:

  • la AOP,

  • la gestión de mensajes i18n a través de MessageSource (consulte el capítulo Configuración avanzada),

  • la propagación de eventos a través de la clase ApplicationEvent y la interfaz ApplicationListener (consulte el capítulo Configuración avanzada),

  • la herencia de contextos jerárquicos,

  • el acceso a recursos como URL o archivos,

  • una especialización de contexto (WebApplicationContext).

Los eventos integrados son:

  • ContextRefreshedEvent

  • ContextClosedEvent

  • RequestHandledEvent

  • eventos personalizados con el método publishEvent().

El espacio de memoria en el que Spring almacena sus elementos de configuración y referencias a objetos gestionados se denomina contexto. Si queremos hacer operaciones usando Spring, necesitamos un contexto de manera obligatoria. Este contexto contiene toda la información sobre los beans Spring configurados. También contiene, entre otras cosas, el estado de estos beans a lo largo del tiempo, así como los vínculos entre los beans y las modificaciones dinámicas realizadas en estos beans para la programación basada en aspectos.

Hay dos facetas complementarias: la primera es la configuración de los beans que indica qué objetos de Java se deben considerar como Spring beans, y la segunda se refiere a la inyección de estos beans en las variables de clase o método, es decir, el uso de los beans en el proyecto. Estas nociones se «mezclan» en algún momento, porque, como veremos, podemos hacer referencia a los beans en otros beans dentro de la configuración.

Para recuperar los beans, utilizamos un cargador de contexto.

Para las aplicaciones web utilizamos un ContextLoader, pero el principio sigue siendo el mismo.

10. Relación entre el bean, su BeanFactory o su contexto

Es posible extender el funcionamiento estándar de un bean:

Cuando el bean implementa la interfaz BeanNameAware, Spring llama automáticamente al método void setBeanName(String).

Cuando el bean implementa la interfaz BeanFactoryAware, Spring llama automáticamente al método void setBeanFactory(BeanFactory).

Cuando el bean implementa la interfaz ApplicationContextAware, Spring llama automáticamente al método setApplicationContext (ApplicationContext).

Esto permite recuperar una referencia sobre la parte en cuestión.

He aquí el ciclo de vida de un bean Spring:

  • Inicialización.

  • Actualización de propiedades.

  • Llamada al setBeanName del BeanNameAware.

  • Llamada al conjunto SetBeanFactory del BeanFactoryAware.

  • Llamada al setApplicationContext del ApplicationContextAware.

  • Pre inicialización del bean (BeanPostProcessors) personalizable a través de un BeanFactoryPostProcessor.

  • Llamada a afterPropertiesSet del InitializingBeans.

  • O llamada al método de inicialización personalizado si (@PostConstruct).

  • Posinicialización (BeanPostProcessors).

  • Bean listo para usar.

  • Llamada al método destroy() si implementa DisposableBean.

  • O llamada al método @PreDestroy.

Configuración de los beans Bean:configuración

Como se ha mencionado, la configuración de los beans Spring puede ser implícita o explícita. En modo implícito, Spring reconoce qué clases Java debe añadir a la lista de beans a partir del marcado en las candidatas. En modo explícito, le decimos a Spring el nombre y la ubicación exacta de los beans. El modo explícito es más seguro, pero se tarda más en configurar. Por este motivo rara vez se usa. Los beans se pueden configurar mediante archivos XML, anotaciones o directamente en Java.

Posibilidad de declarar beans

Tipo

Modo

Uso

Archivos XML

Explícito

Beanes personalizados externamente para variación por entorno.

Código Java

Implícito y explícito

Beans de configuraciones estáticas.

Anotaciones

Implícito

Beans que experimentan pocos cambios estructurales.

Lambda

Implícito

Sin proxy (abordaremos este aspecto en detalle).

Por lo tanto, por este medio podemos declarar un árbol de objetos que contenga objetos singleton y objetos prototype. También veremos que es posible tener una configuración diferente para la «run» y para las pruebas que, por ejemplo, tienen una configuración dedicada.

La configuración por XML presenta dificultades relacionadas con el uso de la configuración en formato texto. Spring debe hacer conversiones del texto al tipo de destino. Para indicar un número, lo metemos en una cadena, que posteriormente se convierte. Para indicar el nombre de una variable o argumento, también usamos texto. Los problemas solo son visibles en tiempo de ejecución, mientras que, si se configura en Java, los problemas se revelan directamente durante la fase de compilación.

Por lo tanto, primero usaremos la configuración en Java. Los ejemplos muestran todos los tipos de configuración para presentarlos.

1. Configuración mediante un archivo XML XML

Las aplicaciones sencillas o antiguas se configuran directamente en uno o más archivos XML, organizados de forma jerárquica. Por lo general, tenemos un archivo XML principal por contenedor Spring, junto con archivos XML periféricos.

En el siguiente ejemplo, se muestra cómo declarar un bean miServicio que coincida con la clase de implementación miPaquete.MiServicioImpl.

<Beans> 
           <Bean id="miServicio" class="miPaquete.MiServicioImpl"/> 
        </Beans> 

Si la interfaz de la clase del bean es miPaquete.MiServicio, le diremos a Spring que queremos crear un singleton llamado miServicio. Este singleton se puede inyectar en otro bean, es decir, podemos pedirle a Spring que copie la referencia de este objeto en un miembro de otro objeto de nuestra elección:

<bean id="miContenedor" class="miPaquete.MiContenedorImpl"> 
           <property name="servicio" ref="miServicio" /> 
        </bean> 

Este nuevo singleton miContenedor, que tiene la implementación MiContenedorImpl, contendrá como miembro el objeto Service que, a su vez, contendrá el singleton declarado anteriormente.

Podemos decirle a Spring que descubra automáticamente las clases Java que son candidatas a convertirse en beans Spring, especificando en el archivo XML la lista de paquetes que hay que a escanear:

<context:component-scan base-package="miPaquete" /> 

Es posible utilizar una expresión regular para que no tenga que enumerar paquetes a mano, con lo que el funcionamiento depende entonces de convenciones de nomenclatura que tendrán que estar bien documentadas. En caso de error, podemos tener beans de Spring que no eran candidatos; por ejemplo, beans destinados a pruebas y que aún están en producción. En este nivel, la prudencia nos obliga a dotarnos de medios de control para evitar ciertos abusos. 

2. Configuración con anotaciones Anotaciones

Esta reciente evolución de Spring permite configurar Spring directamente en Java de una manera más flexible.

Es habitual que, además de la configuración de Java, tengamos un archivo de configuración XML considerablemente más ligero. Nos aseguraremos de agrupar los elementos de configuración en un solo paquete tanto como sea posible.

El uso de anotaciones para configurar el equivalente del ejemplo anterior, nos da el siguiente código:

package miPaquete;  
        @Configuration 
        public class AppConfig { 
           @Bean 
           public MiServicio myService() { 
              return new MiServicioImpl(); 
           } 
        } 

Esta instrucción indica a Spring que el bean con el id miServicio tiene la clase de implementación MiServicioImpl del paquete miPaquete. Calificamos el tipo de objeto devuelto por su interfaz. Esto especifica que cualquier objeto declarado por esta interfaz puede recibir este servicio por inyección. Es posible poner otro nombre para el bean usando la anotación @Qualifier.

La anotación @Qualifier

class Pollo implements AveCorral{} 
        class Oca implements AveCorral {} 

Podemos elegir la implementación especificando el nombre del bean:

@Autowired 
        Pollo(@Qualifier("pollo") AveCorral aveCorral) { 
          this.aveCorral = aveCorral; 
        } 

El nombre se puede especificar en el objeto, su setter o a través del constructor.

También controlamos el proceso de instanciación del objeto, lo que nos permite personalizar esta creación del objeto. Así que vemos que esta forma de hacer las cosas es mucho más potente que la simple utilización de XML. En caso de problemas, también podemos trazar y depurar el proceso de creación del contexto de la aplicación.

Lista de las principales anotaciones para configurar los beans a partir de anotaciones:

Anotación

Efectos

@Configuration en la clase

La clase anotada @Configuration corresponde a una factory de beans (desing patter factory).

@Bean sobre el método

El método anotado @Bean sustituye al elemento de configuración XML <Beans/> del archivo de configuración XML.

@ComponentScan

Podemos usar la anotación @ComponentScan junto con la anotación @Configuration para decirle a Spring que debe escanear los paquetes especificados en la anotación usando el argumento basePackages.

La anotación @ComponentScan:

@ComponentScan(); 

@ComponentScan sin argumentos le dice a Spring que analice el paquete actual y todos sus subpaquetes.

Podemos afinar para especificar los paquetes como argumento de la anotación:

@ComponentScan(basePackages={"paquete1", " paquete2"}) 

3. Configurar los beans de aplicación implícitamente Bean

Por lo tanto, nos podemos basar en una configuración XML o Java para configurar los beans automáticamente, utilizando una facilidad de Spring que permite descubrir los beans basados en un paquete básico y en expresiones regulares.

La declaración implícita es la forma más sencilla de configurar los beans de aplicación. Permite no declararlos uno por uno. En el archivo de configuración XML o en la configuración de Java, especificamos una regla basada en una expresión regular para identificar los beans Spring etiquetados por anotaciones.

La anotación asociada con el bean más simple es @Component. Es posible utilizar la nueva implementación de JSR-330: Dependency Injection for Java, que proporciona anotaciones cuyo funcionamiento se parece al de Spring porque Spring ha interactuado con estas nuevas librerías, pero estas anotaciones son menos avanzadas que las de Spring.

Elegiremos anotaciones JSR-330 @Inject para los casos sencillos y usaremos la anotación Spring @Autowired para los proyectos complejos porque el atributo required no existe para @Inject.

Anotación

Utilidad

@Component

Anotación genérica para todos los beans.

@Repository

Anotación para los beans de tipo DAO para la capa de persistencia.

@Service

Anotación para los beans de tipo Service para la capa de servicio.

@Controller

Anotación para los controladores MVC para la capa de presentación SpringMVC.

También hay otras anotaciones que indican si el bean está sujeto a AOP o aspectos transaccionales.

4. Configuración por lambdas

Podemos registrar un bean con una lambda:

context.registerBean(MiServicio.class, () -> new MiServicio()); 

Podemos registrar un bean con una lambda añadiendo un nombre significativo:

context.registerBean("miSegundoServicio", MiServicio.class, 
        () -> new MiServicio()); 

También es posible personalizar el bean añadiendo un callback que posicione una propiedad del bean, como el flag autowire-candidate:

context.registerBean("miCallbackServicio", MiServicio.class, 
        () -> new MiServicio(), s -> s.setAutowireCandidate(true)); 

Uso de beans: inyección para setters y constructores Bean:inyección Setter

La aplicación dispone del bean cuando este se declara en la configuración. Entonces, podemos inyectarlo como miembro de nuestra clase.

Spring solo puede inyectar dependencias en un bean de Spring.

Los expertos dirán que, mediante la configuración de la JVM, es posible inyectar los beans donde quiera modificando el cargador de clases, pero, en un contexto de uso normal, nos limitaremos a los beans Spring.

Por lo tanto, el primer bean de nuestra aplicación se instanciará obligatoriamente por medio de una factory Spring que cargará el contexto y aplicará la configuración a ese bean. Este es el punto de partida de Spring en la aplicación.

Spring crea el objeto con el constructor predeterminado y, a continuación, llama a los setters para inyectar las referencias de los objetos que se van a inyectar. Si el constructor predeterminado no está disponible, Spring utiliza un constructor con argumentos, que también rellena las dependencias. Esta posibilidad se usa para las API externas a Spring que no tienen constructores predeterminados, como los pools de conexiones, para los que debemos pasar el datasource como argumento constructor.

Un constructor que contiene beans como argumentos permite hacer el equivalente de la anotación @Autowired en los miembros del bean. Por otro lado, es necesario tener un número limitado de argumentos en el constructor para mantener la legibilidad.

La anotación @Autowired se coloca en la variable o, preferiblemente, en el setter de la variable. Es posible especificar que la resolución se puede diferir al inicio a través del argumento required=false:

@Autowired(required=false) 

Esto permite indicar a Spring que el bean que se va a mapear puede no estar disponible durante el inicio. Por ejemplo, si durante la instanciación (carga en la memoria) de un bean se necesita un elemento que no está disponible al inicio, es posible posponer su instanciación al momento en el que se utilice por primera vez. Un bean puede necesitar que se inicie la base de datos o que ya se haya producido una conexión a ella, por ejemplo, antes de que se pueda utilizar.

En caso de que no necesitemos utilizar la carga diferida, también es posible utilizar la anotación @Inject del JSR-330.

1. Asignación mediante el constructor en XML Mapping XML

El elemento <constructor-arg> o la tecnología c-namespace más compacta introducida en Spring 3 se utiliza para hacer coincidir la firma del constructor en una configuración XML:

<Bean id="disquete" class="unidad.DISQUETE"> < 
         <constructor-arg ref="format" /> 
        </Bean> 

o:

<Bean id=" disquete " class="unidad.DISQUETE" 
        c:cd-ref="format" /> 

Para el «c-namespace», es posible determinar el nombre de la variable constructora por su nombre o por su índice, con un subíndice implícito si solo hay un argumento.

Sintaxis

Determinante

c:_nombredelavariable="valor"

Un nombre

c:_0="valor"

Un índice

c:_="valor"

Implícito porque solo hay un argumento

Spring traduce automáticamente del tipo String al posible tipo detectado en la lista de constructores disponibles porque el valor está en String en el archivo de configuración. El transtipado de tipos es arriesgado porque puede ser la fuente de muchos errores; el equivalente en Java de la configuración XML a través de anotaciones es mucho más fiable.

Es posible tener mappings con los constructores que tienen argumentos de tipo colección. El bean se puede instanciar y asignar sus valores de manera automática. Esto es muy práctico, por ejemplo, para crear un datasource preconfigurado.

<Bean id="dataSource" 
           class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close" 
          p:driverClassName="oracle.jdbc.driver.OracleDriver" 
          p:url="jdbc:oracle:thin:@localhost:1521:xe" 
          p:username="MiUsuarioDePrueba" 
          p:password="LaContraseñaDeMiUsuarioDePrueba" 
        /> 

2. Comparación de los métodos de instanciación Instanciación

Beneficios

Por setter

Por constructor

Por campo

JavaBean

No

No

Herencia

No

No

Claridad

Alto

Medio

Bajo

Flexibilidad

Alto

Bajo

Alto

Concisión

Bajo

Alto

Muy alto

Debilidad

Pruebas unitarias

Ninguno

Ninguno

En la mayoría de los casos, se utilizará la inyección basada en el constructor. La inyección por campo se debe evitar porque es más difícil de mantener, pero algunas veces es necesaria en algunos frameworks técnicos y se realiza preferentemente en el archivo XML para mayor claridad.

3. Otras anotaciones de configuración

Hay otras anotaciones relacionadas con la configuración.

a. La anotación @Primary

Esta anotación permite tener dos beans con la misma definición y le dice a Spring cuál se debe seleccionar con prioridad. Esto permite sustituir un bean por otro para pruebas, por ejemplo.

En el lado de la aplicación:

@Component ("pojo"public class PojoImpl implements Pojo { 
          @Override 
          public String getNombre() { 
          return "Pojo"; 
          } 
        } 

En el lado de las pruebas:

@Component("pojo"@Primary 
        public class OtroPojoImpl implements Pojo { 
          @Override 
          public String getNombre() { 
          return "Modificado"; 
          } 
        } 

Hay dos componentes pojo en tiempo de ejecución, Spring toma el que tiene la anotación @Primary.

También permite cambiar un elemento de configuración de una clase anotada a través de la anotación @Configuration.

@Configuration 
        public class Config { 
          @Bean 
          public Novio JohnEmployee() { 
          return new Novio("Toto"); 
          } 
          @Bean 
          @Primary 
          public Novio TonyEmployee() { 
          return new Novio("Titi"); 
          } 
        } 

En tiempo de ejecución:

AnnotationConfigApplicationContext context 
          = new AnnotationConfigApplicationContext(Config.class); 
         
        Novio novio = context.getBean(Novio.class); 
        System.out.println(novio); 

muestra con preferencia Titi.

Podemos utilizar esta posibilidad en caso de que tengamos, por ejemplo, un WAR que encapsule los JAR. Estas configuraciones pueden entrar en conflicto y priorizaremos, por ejemplo, la de WAR añadiendo un @Primary sobre elementos conflictivos.

b. Las anotaciones @Profile y @Conditional

Anotación @Profile

Las anotaciones que se utilizan ampliamente en la configuración automática de Spring Boot permiten activar beans o elegir un bean específico para un perfil o elemento de configuración.

@Component 
        @Profile("dev"public class DevConfig implements DatasourceConfig {...} 
         
        @Component 
        @Profile("prod"public class ProdConfig implements DatasourceConfig {...} 

En tiempo de ejecución, en función del perfil, Spring cargará el bean de configuración de dev o de prod.

El bean también se puede configurar en XML:

<beans profile="dev"> 
         <bean id="devConfig" 
         class="fr.eni.DevConfig" /> 
        </beans> 

El perfil se puede definir de varias maneras:

A través de la interfaz WebApplicationInitializer para aplicaciones web:

@Configuration 
        public class MiWebApplicationWebInitializer 
          implements WebApplicationInitializer { 
         
          @Override 
          public void onStartup(ServletContext servletContext) throws 
        ServletException {    
          servletContext.setInitParameter( 
            "spring.profiles.active", "dev"); 
          } 
        } 

A través de una configuración de contexto web.xml:

<context-param> 
          <param-name>contextConfigLocation</param-name> 
          <param-value>/WEB-INF/app-config.xml</param-value> 
        </context-param> 
        <context-param> 
          <param-name>spring.profiles.active</param-name> 
          <param-value>dev</param-value> 
        </context-param> 

A través de una variable de entorno, por ejemplo, en Linux:

export spring_profiles_active=dev 

A través de un argumento de lanzamiento de la JVM:

-Dspring.profiles.active=dev 

En el código, a través de:

@Autowired 
        private ConfigurableEnvironment env;  
        env.setActiveProfiles("dev"); 

Es posible activar un perfil específico en las pruebas:

@ActiveProfiles("dev") 

Es posible recuperar el perfil activo:

String perfilActivo = environment.getActiveProfiles(); 

Podemos inyectar una variable que consulta el perfil activo:

@Value("${spring.profiles.active}"private String perfilActivo; 

El uso de la anotación @Value puede causar problemas para las pruebas unitarias Mockito fuera de Spring porque la variable es private y se debe valorar para la prueba. Luego usamos un setter o cambiamos la accesibilidad de la variable: 

Field f = obj.getClass().getDeclaredField("perfilActivo"); 
        f.setAccessible(true); 

o usamos una función de Spring dedicada:

Field field = ReflectionUtils.findField(MyEntity.class, "createDate"); 
        field.setAccessible(true); 

Anotación @Conditional

La anotación @Conditional se trata específicamente en el capítulo relativo a Spring Boot. Permite elegir de manera condicional un bean, entre los candidatos que tienen el mismo nombre. A menudo, se usa junto con la anotación @Primary para sobrecargas.

Se puede usar en un bean Spring estándar o en un bean de configuración Spring.

La condición se realiza en presencia o ausencia de una propiedad de configuración.

Bean estándar:

@Bean  
        @ConditionalOnProperty( 
          name = "features.calcul.experimental", 
          matchIfMissing = truepublic class calculadorPorDefecto implements Calculador { 
        [...] 
        } 
         
        @Bean  
        @ConditionalOnProperty( 
          name = "features.calcul.experimental"public class CalculadorExperimental implements Calculador { 
        [...] 
        } 

Bean de configuración:

@Configuration 
        public class MiPrimeraConfig { 
         @Bean  
          @ConditionalOnProperty( 
          name = "features.calcul.experimental", 
          matchIfMissing = true) 
          public Calculador calculadorPorDefecto() { 
          return new calculadorPorDefecto() ; 
          } 
         
          @Bean  
          @ConditionalOnProperty( 
            name = "features.calcul.experimental") 
          public Calculador calculadorExperimental() { 
            return new CalculadorExperimental () ; 
          } 
        } 

Podemos usar esta funcionalidad para tener dos modos activados a través de un argumento de configuración.

La anotación @DependsOn

Podemos usar esta anotación para que Spring inicialice otros beans antes que el anotado. Este comportamiento suele ser automático, basado en dependencias explícitas entre los beans. En el caso de dependencias implícitas, podemos usar esta anotación, por ejemplo, cuando se carga el controlador JDBC o se inicializan variables estáticas:

@DependsOn("connectionActive"class implements StatusDeLaBase {} 

o

@Bean 
        @DependsOn("connectionActive"Status status() { 
          return new Status(); 
        } 

La anotación @Lazy

Usamos @Lazy cuando queremos inicializar nuestro bean más adelante. De forma predeterminada, Spring crea todos los beans singleton cuando se inicia el contexto de la aplicación y, algunas veces, tenemos que diferir su creación.

Se puede colocar en varios lugares:

  • En un método de la factory de beans anotado @Bean, para retrasar la llamada del método.

  • En una clase @Configuration, todos los métodos @Bean contenidos se ven afectados.

  • En una clase @Component, que no es una clase @Configuration, este bean se inicializará más tarde.

  • En un constructor, setter o campo @Autowired, para cargar la dependencia de manera retrasada (a través del proxy).

Su argumento «value» es true por defecto.

Por ejemplo, para una sonda que requiere que la parte sondeada se inicie y esté operativa:

@Configuration 
        @Lazy 
        class SondaFactoryConfig { 
          @Bean 
          @Lazy(false) 
          Sonde sonda() { 
            return new Sonda(); 
          } 
        } 

La sonda se configura en el último momento, cuando se utiliza.

La anotación @Scope

Usamos @Scope para definir el alcance de una clase @Component o de una definición @Bean. Define la visibilidad del bean en su contexto.

Hay ámbitos o scopes predefinidos, como singleton, prototype, query, session, global session y custom scopes:

Scope

Definición

singleton

De forma predeterminada, design pattern singleton.

prototype

Una invocación usando el bean.

request

Relacionado con una petición: una instancia de bean para una sola petición HTTP.

session

Relacionado con una sesión: una instancia de bean para una sesión HTTP completa.

application

Relacionado con una aplicación: una instancia de bean para el ciclo de vida de un ServletContext.

websocket

Relacionado con un websocket: una instancia de bean para una sesión de WebSocket concreta.

Personalizado

Scope personalizado.

Si tenemos un bean que genera un top:

@Data 
        public class TopMessageGenerator { 
          private String top; 
        } 

Tendremos:

Scope request

@Bean 
        @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = 
        ScopedProxyMode.TARGET_CLASS) 
        public TopMessageGenerator requestScopedBean() { 
          return new TopMessageGenerator(); 
        } 

O más sencillo:

@Bean 
        @RequestScope 
        public TopMessageGenerator requestScopedBean() { 
          return new TopMessageGenerator(); 
        } 

El controlador:

@Controller 
        public class ScopesController { 
          @Resource(name = "requestScopedBean") 
          TopMessageGenerator requestScopedBean; 
         
          @RequestMapping("/scopes/request") 
          public String getRequestScopeMessage(final Model model) { 
          model.addAttribute("previousMessage", requestScopedBean.getMessage()); 
          requestScopedBean.setTop(LocalTime.now().toString()); 
          model.addAttribute("currentMessage", requestScopedBean.getMessage()); 
          return "scopesExample"; 
         } 
        } 

Scope session

Un bean de scope session:

@Bean 
        @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = 
        ScopedProxyMode.TARGET_CLASS) 
        public TopMessageGenerator sessionScopedBean() { 
            return new TopMessageGenerator(); 
        } 

O más simplemente:

@Bean 
        @SessionScope 
        public TopMessageGenerator sessionScopedBean() { 
            return new TopMessageGenerator(); 
        } 

Y su controlador:

@Controller 
        public class ScopesController { 
          @Resource(name = "applicationScopedBean") 
          TopMessageGenerator applicationScopedBean; 
         
          @RequestMapping("/scopes/application") 
          public String getApplicationScopeMessage(final Model model) { 
            model.addAttribute("previousMessage", 
        applicationScopedBean.getMessage()); 
            applicationScopedBean.setMessage(LocalTime.now().toString()); 
            model.addAttribute("currentMessage", 
        applicationScopedBean.getMessage()); 
            return "scopesExample"; 
          } 
        } 

La anotación @Lookup

Cuando llamamos a un método anotado con @Lookup, este le dice a Spring que devuelva una instancia del tipo de retorno del método. Esto se utiliza principalmente para inyectar un bean prototype en un bean singleton y para hacer una inyección de dependencias de manera procedimental.

Inyectamos un bean de scope prototype en un bean singleton cuando queremos hacer referencia a un bean de tipo prototype en un bean estándar Spring que es un singleton:

@Component 
        @Scope("prototype"public class Notificacion { 
          // mis estados prototype-scoped 
        } 
         
        @Component 
        public class ServiciosAplicativos { 
         
          // ... 
         
          @Lookup 
          public Notificacion getNotificacion() return null; 
          } 
         
          // ... 
         
        @Test 
        public void whenLookupMethodCalled_thenNewInstanceReturned() { 
          ServiciosAplicativos primero = 
        this.context.getBean(ServiciosAplicativos.class); 
          ServiciosAplicativos second = 
        this.context.getBean(ServiciosAplicativos.class); 
         
          assertEquals(first, second); //singleton 
          assertNotEquals(first.getNotificacion(), second.getNotificacion()); 
        } 

La notificación, que es un prototipo, se crea durante la llamada como a través de un stub.

En el método devolvemos null porque, como está sobrecargado, es esta sobrecarga la que devuelve el valor. Es raro que se utilice esta anotación.

La anotación @Value

Podemos usar la anotación @Value para inyectar una propiedad en un bean en la propiedad, el setter o el constructor.

Por ejemplo, para una inyección a través del constructor:

Helice(@Value("3") int numeroPalas) { 
          this.numeroPalas = numeroPalas; 
        } 

Este valor puede usar el sistema del placeholder si configuramos PropertyPlaceholderConfigure o PropertySourcesPlaceholderConfigurer.

Se explican más adelante otros ejemplo en una sección específica de SpEL (Spring Expression Language).

La anotación @Required

La anotación @Required se puede utilizar en un setter para especificar que desea que el valor se rellene mediante la configuración:

@Required 
        void setPeriodo(String periodo) { 
          this.periodo = periodo; 
        } 
        <bean class="fr.eni.Parte"> 
          <property name="period" value="mañana" /> 
        </bean> 

Si no se rellena el valor, se produce la excepción BeanInitialization Exception

La anotación @Import

Podemos usar las clases @Configuration específicas sin usar el component scan.

@Import(MiConfig.class) 
        clase TestConfig {} 

Esta anotación permite importar una configuración en formato XML, con las anotaciones basadas en @Configuration o en ImportSelector.

Un ImportSelector es una interfaz:

public interface ImportSelector { 
               String[] selectImports(AnnotationMetadata importingClassMetadata); 
               @Nullable 
               default Predicate<String> getExclusionFilter() { 
                      return null; 
               } 
        } 

que devuelve una tabla con una lista de clases que se corresponden con las configuraciones.

Principalmente, esto permite pruebas unitarias con una configuración específica en un bean.

La anotación @ImportResource

Podemos importar configuraciones XML con esta anotación:

@Configuration 
        @ImportResource("classpath:/anotaciones.xml"class SondaFactoryConfig {} 

Esto permite principalmente pruebas unitarias con una configuración específica en un bean.

Control del ciclo de vida: construcción y destrucción Ciclo de vida

Spring permite interceptar la creación y descripción de los objetos Spring.

No es posible utilizar los beans inyectados directamente en el constructor porque, en este preciso momento, Spring aún no ha instanciado los objetos contenidos en los miembros de la clase. Del mismo modo, la llamada al método finally no es sencilla porque es el garbage collector quien lo llama y el finally está deprecated (JEP 421). Spring da la posibilidad de marcar métodos para que se llamen durante la creación y destrucción del objeto. Hay varios mecanismos.

He aquí el más reciente.

Anotación

Momento de la llamada

@PostConstruct

Después del constructor, con los beans inicializados.

@PreDestroy

Antes de la destrucción.

También es posible utilizar aspectos de la programación orientada a aspectos (AOP) para realizar acciones durante la creación y destrucción del objeto.

Antes de estas anotaciones, usamos métodos parametrizados en la configuración con los argumentos init-method y destroy-method, como en el siguiente ejemplo:

<bean id="helloWorld" 
           class="com.tutorialspoint.HelloWorld"  
           init-method="init" destroy-method="destroy"> 
           <property name="mensaje" value="Hola"/> 
        </bean> 

Esta manera de proceder todavía funciona, pero es más sencillo usar las anotaciones.

Ejemplo que ilustra los mappings estándares Mapping

Este ejemplo describe, paso a paso, la creación de un proyecto sencillo. Está presente en los ejemplos que se pueden descargar en la página Información. En primer momento, este capítulo puede parecer un poco complejo para los principiantes, pero se puede utilizar para experimentar.

1. El Proyecto Maven Maven

  • Cree un proyecto vacío de Maven con el siguiente arquetipo:

mvn archetype:generate -DgroupId=fr.eni.editions 
        -DartifactId=mappingApp -DarchetypeArtifactId=maven-archetype-quickstart 
        -DinteractiveMode=false 
  • O use el Spring Initalizr para crear un proyecto vacío (https://start.spring.io/).

  • Importe el archivo Maven a Eclipse o IntelliJ IDEA (o a su herramienta de desarrollo favorita).

  • Para separar los archivos de configuración de los archivos de código, cree los directorios de origen:

  • src/main/resources

  • src/test/resources

Pondremos en estos directorios los archivos de configuración de Spring y el sistema de registro.

2. Archivo de configuración de Spring Archivo de configuración

Crearemos un archivo Spring llamado applicationContext.xml y lo colocaremos en el directorio src/main/resources/Spring/.

<?xml version="1.0" encoding="UTF-8"?> 
        <beans default-lazy-init="true" 
        xmlns="http://www.springframework.org/schema/beans" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context" 
        xmlns:jdbc="http://www.springframework.org/schema/jdbc" 
           xsi:schemaLocation=" 
             http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-4.1.xsd 
             http://www.springframework.org/schema/jdbc 
        http://www.springframework.org/schema/jdbc/spring-jdbc-4.1.xsd 
             http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-4.1.xsd"> 
           <context:component-scan base-package="fr.eni" /> 
        </beans> 

En este archivo, indicamos que los beans anotados para tener en cuenta están en el paquete "fr.eni" y en todos sus subpaquetes.

3. Dependencia de Spring Core y sistema de registro Spring Core Log

Spring es autónomo en términos de dependencias, con la excepción del framework de registros de actividad, de manera que se pueda utilizar el sistema de registro del proyecto integrando el motor Spring. Dependencias

Spring utiliza por defecto el sistema commons-logging de Spring Core. Para tener su propio sistema de logging, debe excluir el common.logging y sustituirlo por Logback porque, de hecho, es mucho más práctico y más potente que Log4j.

Usaremos SLF4J (http://www.slf4j.org/) para interactuar con Logback en las siguientes versiones:

  • SLF4J en la versión 1.7.36.

  • Logback en versión 1.2.11.

  • Spring Core versión 5.3.18.RELEASE.

Esto proporciona las siguientes dependencias en el archivo pom.xml:

   <dependency> 
          <groupId>org.springframework</groupId> 
          <artifactId>spring-core</artifactId> 
          <version>5.3.18.RELEASE</version> 
          <exclusions> 
             <exclusion> 
              <groupId>commons-logging</groupId> 
              <artifactId>commons-logging</artifactId> 
            </exclusion> 
          </exclusions> 
          </dependency> 

4. Dependencia de librerías de pruebas unitarias Pruebas:unitarias

Veremos los detalles de las pruebas unitarias y las pruebas de integración en un capítulo dedicado. De hecho, Spring es muy práctico para hacer pruebas. Usaremos jUnit junto con org.easytesting para este ejemplo. Utilizando el scope <test>, indicamos que las dependencias de este scope solo estarán presentes durante las pruebas.

Extracto del archivo pom.xml

   <dependency> 
          <groupId>junit</groupId> 
          <artifactId>junit</artifactId> 
          <version>4.13.2</version> 
          <scope>test</scope> 
           </dependency> 
           <dependency> 
          <groupId>org.easytesting</groupId> 
          <artifactId>fest-assert</artifactId> 
          <version>1.4</version> 
           <dependency> 

Spring-test y Spring-context también se deben integrar en el scope test. De hecho, son específicos de las pruebas:

   <dependency> 
          <groupId>org.springframework</groupId> 
          <artifactId>spring-test</artifactId> 
          <version>5.3.18.RELEASE</version> 
          <scope>test</scope> 
           </dependency> 
           <dependency> 
          <groupId>org.springframework</groupId> 
          <artifactId>spring-context</artifactId> 
          <version>5.3.18.RELEASE</version> 
          <scope>test</scope> 
           </dependency> 

5. Ejemplo que ilustra el uso de los logs Log

Probamos esta configuración añadiendo simples logs en la clase de prueba AppTest:

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration(locations = { 
        "classpath*:spring/applicationContext-test.xml" }) 
        public class AppTest 
        [...] 
           @Test 
           public void testApp(){ 
          logger.info("Basic info Spring Test"); 
          logger.debug("Basic debug Spring Test"); 
          simpleService.printHello(); 
          assertThat( true ); 
           } 

Lo único que tenemos que hacer es modificar el archivo de configuración de los logs logback.xml en el archivo src/tests/resources:

<?xml version="1.0" encoding="UTF-8"?> 
        <configuration> 
        [...] 
           <logger name="fr.eni.editions" additivity="false"> 
          <level value="DEBUG" /> 
          <appender-ref ref="consoleAppender" /> 
           </logger> 
        </configuration> 

6. Archivo de configuración específico de las pruebas Archivo de configuración Pruebas

Crearemos un archivo de configuración Spring applicationContext-test.xml para realizar pruebas en /src/test/resources/spring.

<?xml version="1.0" encoding="UTF-8"?> 
        <Beans default-lazy-init="true" 
        xmlns="http://www.springframework.org/schema/Beans" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context" 
        xmlns:jdbc="http://www.springframework.org/schema/jdbc" 
           xsi:schemaLocation=" 
             http://www.springframework.org/schema/Beans 
        http://www.springframework.org/schema/Beans/Spring-Beans-4.1.xsd 
             http://www.springframework.org/schema/jdbc 
        http://www.springframework.org/schema/jdbc/Spring-jdbc-4.1.xsd 
             http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/Spring-context-4.1.xsd"> 
           <import resource="classpath*:Spring/applicationContext.xml" /> 
        </Beans> 

Este archivo se parece al archivo principal. Por ahora, solo estamos importando el archivo principal. En este archivo podremos añadir los elementos de configuración específicos de las pruebas.

El registro de la actividad se genera cuando el programa se ejecuta:

15:45:56.278 [main] INFO fr.eni.editions.AppTest - Basic Spring Test 

Es posible ver todos los logs de depuración de Spring descomentando el appender de los logs de Spring: el archivo de log generado tiene una longitud de más de cien líneas.

Un buen ejercicio será leer este log en detalle.

Es posible cargar la javadoc y los recursos de dependencias Maven desde la configuración Maven del proyecto. A continuación, podemos seguir paso a paso la ejecución de los métodos del motor Spring para descubrir su funcionamiento interno. Este es un ejercicio muy bueno para ver parte del funcionamiento interno del framework.

Si está interesado, puede descargar las fuentes de Spring para hacer un proyecto independiente, que incluirá como dependencia de su proyecto. Esto le permitirá buscar código, poner puntos de interrupción y, especialmente, añadir los logs o realizar pequeñas modificaciones para entender lo que está sucediendo.

Spring Expression Language

Es un lenguaje de expresión ampliamente utilizado que permite variabilizar un argumento que se fijaría en una cadena, lo que es particularmente útil con argumentos de anotación.

Su sintaxis es similar a Jakarta Expression Language (Unified EL), pero ofrece posibilidades adicionales, como la invocación de métodos en un bean o en un objeto fuera de Spring.

Es posible utilizar SpEL de forma autónoma usando algunas clases de la infraestructura del núcleo, como el analizador, pero, en general, su uso es transparente cuando codificamos con Spring. SpEL está muy cerca de las especificaciones JavaBeans.

1. Uso de ExpressionParser

ExpressionParser se utiliza para evaluar una expresión.

Rara vez lo usamos directamente, pero Spring lo utiliza muy a menudo con las cadenas que le enviamos.

ExpressionParser parser = new SpelExpressionParser (); 
        Expression exp = parser.parseExpression ( "'Hola a'.concat 
        (' todos')"); 
        String message = (String) exp.getValue (); 

Por ejemplo, aquí concatenamos las cadenas «hola a » y «todos».

Se puede acceder a los miembros y métodos de los objetos a través de una cadena de llamadas, utilizando «.».

Por ejemplo:

"'Hola a todos'.bytes.length" 

invoca ’getBytes().length’.

El método getValue tiene la firma:

public <T> T getValue(Class<T> desiredResultType). 

Esto hace posible no especificar el tipo.

Normalmente, usamos ExpressionParser a través de un objeto pivote o raíz:

Coche coche = new Coche ( "Ford" , "blanco" ); 
        ExpressionParser parser = new SpelExpressionParser (); 
        Expression exp = parser.parseExpression ( "marca" ); 
        EvaluationContext context = new StandardEvaluationContext (coche); 
        String marca = (String) exp.getValue (context); 

A continuación, getValue se realiza en un EvaluationContext específico.

2. EvaluationContext

Si no se especifica de manera explícita, de forma predeterminada usamos Standard EvaluationContext, que utiliza la reflexividad y la introspección de Java para manipular objetos.

Con StandardEvaluationContext podemos especificar el objeto raíz que se debe evaluar usando el método setRootObject() o pasando el objeto raíz al constructor.

StandardEvaluationContext elcontext = new StandardEvaluationContext(); 
        TestBean tb = new TestBean(); 
        tb.aaa = "cccc"; 
        Map map = LangUtils.asMap("ccc", "dddd", "userName", "testUser"); 
        map.put("map", LangUtils.asMap("ccc", "dddd", "tb", tb)); 
        elcontext.setRootObject(map); 
        Expression exp = parser.parseExpression("['ccc']"); 
        Object val = (String)exp.getValue(elcontext, String.class); 
        System.out.println("val: " + val); 
        Assert.assertEquals("dddd", val); 

También es posible especificar variables y funciones que se utilizan en la expresión, con ayuda de los métodos setVariable() y registerFunction().

A continuación, se muestra el método setVariable():

ExpressionParser parser = new SpelExpressionParser(); 
        Expression spelExpression = parser.parseExpression("#aMap['un'] eq 1"); 
        StandardEvaluationContext ctx = new StandardEvaluationContext(); 
        ctx.setVariables(new HashMap<String, Object>() { 
        { 
          put("aMap", new HashMap<String, Integer>() { 
               { 
                 put("uno", 1); 
                 put("dos", 2); 
                 put("tres", 3); 
               } 
          }); 
        } 
        }); 
        boolean resultado = spelExpression.getValue(ctx, Boolean.class); 
        assertTrue(resultado); 

Para registerFunction():

SpelExpressionParser parser = new SpelExpressionParser(); 
        StandardEvaluationContext ctx = new StandardEvaluationContext(); 
        ctx.registerFunction("repeat",ExpressionLanguageScenarioTests.class. 
        getDeclaredMethod("repeat",String.class)); 
        Expression expr = parser.parseRaw("#repeat('hello')"); 
        Object value = expr.getValue(ctx); 
        assertEquals("hellohello", value); 

Finalmente, con StandardEvaluationContext podemos registrar los ConstructorResolvers, MethodResolvers y PropertyAccessors personalizados para ampliar la forma en que SpEL evalúa las expresiones.

3. Uso con @Value

En la actualidad, SpEL se utiliza principalmente a través de la anotación @Value

Su uso más sencillo es a través del #, que indica que queremos una evaluación:

@Value("#{1 + 2}") // 3 
        private double adicion; 
         
        @Value("#{0 == 0}") // true 
        private boolean equal; 

Usando un regex:

@Value("#{'123abc' matches '\\d+' }") // false 
        private boolean resultadoNoNumerico; 

Acceso a un miembro de bean:

@Value("${jdbc.url}") 

El bean puede tener sus valores asignados en un archivo de propiedades a través de PropertySourcesPlaceholderConfigurer.

El acceso a un miembro estático de un objeto estático:

@Value("#{ObjetoEstatico.variableEstatica}") 

Servidores J2EE, Java EE y Jakarta EE

1. Aspectos generales

Spring fue diseñado originalmente para evitar el uso de los EJB, que originalmente eran inmaduros. En sus primeras versiones, Spring permitía una gestión simplificada de los EJB. Incluso hoy en día, los proyectos continúan usando Spring 3 con servidores Java JEE 6, o incluso Spring 2 asociado con un servidor Java JEE 5, generalmente con un framework casero. Algunas veces se evita la migración si las modificaciones de código son menores. Encontramos en Spring 2 y 3 casi todo lo que hay en Spring 4, excepto las anotaciones. De hecho, en la medida de lo posible, Spring ha intentado llevar las evoluciones de la versión actual en las versiones anteriores. En todas sus versiones, Spring funciona muy bien en la mayoría de los servidores J2EE, Java EE y Jakarta EE (siempre que utilice la versión que corresponda al servidor empleado).

En la práctica, los «problemas» llegan con bastante rapidez. Para empezar, están las implementaciones «propietarias» de las JVM. Una JVM de IBM no tiene los mismos errores o fallos que una JVM de Oracle. Esto significa que el código no es realmente portable. También es necesario tener en cuenta el ritmo de las versiones correctivas y las dificultades de hacer que los parches estén disponibles en los diferentes entornos.

Luego están las partes personalizadas: WebSphere, WebLogic y jBoss. Estas proponen una parte común desde un punto de vista de las especificaciones, pero cada una tendrá sus sutilezas para implementarlas y, por lo tanto, será necesario conocerlas. Además, también ofrecen partes diferenciadoras que facilitarán enormemente la vida de los desarrolladores, pero los vincularán permanentemente con un proveedor de servidores. Su objetivo es vender un paquete con máquinas, licencias, etc. Estos puentes, generalmente, permiten llamar al código «Legacy» desde el código Java.

IBM ofrece un puente con sus mainframes que permiten utilizar IMS, CISC, etc., junto con Java. WebLogic ofrece un puente con Tuxedo y workflows basados en MDB (Message Driven Bean). JBoss propone innovaciones, JBoss Rules, Hibernate o pocas interconexiones propietarias. JBoss se utiliza mucho en contenedores docker con Kubernetes y OpenShift.

En función del cliente que tiene que elegir un servidor de aplicaciones específico, buscamos a los especialistas informáticos adecuados; por ejemplo, una implementación de IBM o JBoss desde el desarrollador hasta el arquitecto. No había servidores genéricos que se pudieran usar en producción.

Spring se centró en las áreas comunes de estos servidores empresariales, con código portable. Como ejemplo, podemos considerar un administrador de transacciones que funciona de forma diferente en diferentes servidores. Spring se encarga de interactuar con los diferentes servidores. Por ejemplo, Juergen Hoeller del equipo Spring, adaptó el código de Spring para que funcionara con todas las versiones de WebSphere, a pesar de que la gestión de transacciones en el equipo de Spring ha cambiado drásticamente entre diferentes versiones.

Nuestros clientes que utilizan Spring podrían recurrir a especialistas informáticos generalistas, interoperativos y polivalentes, desde desarrolladores hasta arquitectos.

Desde el inicio, Spring se ha integrado con J2EE. Java ha tomado algunos elementos de Spring, pero estamos lejos de ello. Spring puede innovar de manera más simple porque es un framework independiente de los servidores de aplicaciones. Los utiliza, se basa en ellos, pero no necesita probar o hacer compatibles sus evoluciones con librerías específicas. Solo se hace evolucionar una versión, en lugar de una por fabricante. Las facades de Spring (o factory) son «más simples» que el código llamado por estas.

Los servidores Java EE a menudo han adaptado partes importantes de las JVM, como el class loader (cargador de clases), la gestión de memoria, los procesos, etc., para sus propias necesidades. Por lo tanto, es necesario tener en su estación de desarrollo y en su plataforma de integración continua un entorno muy cercano al utilizado en producción para evitar efectos secundarios. Por ejemplo, la administración de charsets UTF-8 y de certificados pueden provocar comportamientos no deseados.

Así que Java EE evolucionó tomando ideas de Spring. Hoy en día encontramos gran parte de las innovaciones de Spring incluidas en Java EE. Los conceptos son similares, pero la implementación es diferente.

Aunque Spring puede presumir de fiabilidad por disponer de millones de usuarios, algunas veces tenemos implementaciones relativamente jóvenes e incompletas de los conceptos en Java EE. El sitio web https://stackoverflow.com, generalmente, tiene una respuesta a los problemas relativos a Spring, pero este no siempre es el caso para los servidores Java EE.

Ahora es posible crear una aplicación Jakarta EE sin librerías propietarias, sin Spring, sin librerías de terceros como las librerías Struts, Apache (commons, etc.), sin SPA (Single Page Applications). Algunas tienen éxito, pero es raro.

Java EE es un conjunto de especificaciones que los proveedores de servidores Java deben implementar. Algunos de estos fueron implementados por servidores ligeros como Tomcat o Jetty. Por lo tanto, hemos aislado esta parte de las especificaciones a través de los perfiles en el webprofile, que es un subconjunto de las especificaciones generales contenidas en la parte completa (platform).

Algunos frameworks, como Hessian, se consideran parte de la esfera Java EE sin tener una especificación.

Tenga en cuenta que las especificaciones de Java para la parte Enterprise todavía se controlan en el lado de Jakarta EE. Spring continúa e integra estas evoluciones de versión.

En la parte webprofile de Jakarta EE 8, encontramos:

  • Jakarta Servlet 4.0

  • Jakarta Server Pages 2.3

  • Jakarta Expression Language 3.0

  • Jakarta Debugging Support for Other Languages 1.0

  • Jakarta Standard Tag Library 1.2

  • Jakarta Server Faces 2.3

  • Jakarta RESTful Web Services 2.1

  • Jakarta WebSocket 1.1

  • Jakarta JSON Processing 1.1

  • Jakarta JSON Binding 1.0

  • Jakarta Annotations 1.3

  • Jakarta Enterprise Beans 3.2 Lite

  • Jakarta Transactions 1.3

  • Jakarta Persistence 2.2

  • Jakarta Bean Validation 2.0

  • Jakarta Managed Beans 1.0

  • Jakarta Interceptors 1.2

  • Jakarta Contexts and Dependency Injection 2.0

  • Jakarta Dependency Injection 1.0

  • Jakarta Security 1.0

  • Jakarta Authentication 1.1

En la parte completa, encontramos una parte obligatoria y otra opcional, además de la parte webprofile:

  • Jakarta Enterprise Beans 3.2

  • Jakarta Enterprise Beans

  • Jakarta Servlet 4.0

  • Jakarta Server Pages 2.3

  • Jakarta Expression Language 3.0

  • Jakarta Messaging 2.0

  • Jakarta Transactions 1.3

  • Jakarta Mail 1.6

  • Jakarta Connectors 1.7

  • Jakarta Enterprise Web Services 1.4 (JAX-WS 1.4)

  • Jakarta RESTful Web Services 2.1 (JAX-RS 2.1)

  • Jakarta WebSocket 1.1

  • Jakarta JSON Processing 1.1

  • Jakarta JSON Binding 1.0

  • Jakarta Concurrency 1.0

  • Jakarta Batch 1.0

  • Jakarta Management 1.1

  • Jakarta Authorization 1.5

  • Jakarta Authentication 1.1

  • Jakarta Security 1.0

  • Jakarta Debugging Support for Other Languages 1.0

  • Jakarta Standard Tag Library 1.2

  • Jakarta Web Services Metadata 2.1

  • Jakarta Server Faces 2.3

  • Jakarta Annotations 1.3

  • Jakarta Persistence 2.2

  • Jakarta Bean Validation 2.0

  • Jakarta Managed Beans 1.0

  • Jakarta Interceptors 1.2

  • Jakarta Contexts and Dependency Injection 2.0

  • Jakarta Dependency Injection 1.0

Como opción:

  • Jakarta Enterprise Beans 3.2

  • Jakarta XML RPC 1.1

  • Jakarta XML Registries 1.0

  • Jakarta Deployment 1.2

En el caso de una aplicación Java desplegada en un servidor Java EE o Jakarta EE, tenemos la opción de una funcionalidad para usar:

  • solo la API de Jakarta EE,

  • la API de Jakarta EE detrás de Spring, a través de una facade o factory,

  • una API de código abierto detrás de Spring, a través de una facade o factory,

  • una API «casera» detrás de Spring, a través de una facade o factory,

  • una API «casera» sin o con Spring, pero oculta en un framework «casero».

Spring interactúa con los servidores que implementan estas especificaciones. Algunas veces, Spring todavía no está interconectado con los componentes correspondientes a la última versión de una especificación.

Spring gestiona en su parte Spring Integration:

  • llamadas remotas y servicios web, RMI, el uso de Hessian y llamadas de servicio a través de HTTP, JMS, AMQP,

  • EJB,

  • JCA CCI,

  • correos electrónicos,

  • schedulers,

  • la caché.

En este capítulo, solo detallamos la parte Web Service JAX-WS y el uso de los EJB. Las llamadas RMI y Hessian son demasiado específicas. Solo hay que recordar que RMI se puede usar para llamar a componentes CORBA y que la especificación RMI-IIOP se elimina en Jakarta EE 9. Por su parte, Hessian permite crear servicios web utilizando archivos binarios.

No detallaremos la interfaz con JMX y JMX remoto (servidor) y JCA CCI, que rara vez se usan fuera de los frameworks.

2. Web services

Para los web services, Spring expone o utiliza los web services JAX-WS a través de su módulo Spring WS, actualmente en la versión 3.1.1.RELEASE, disponible en la dirección https://spring.io/projects/spring-ws

Spring permite exponer un web service a través de un servlet, de un servidor integrado y a través de JAX-WS RI GlassFish (no se detalla aquí porque no es portable).

a. A través de un servlet

Extendemos una clase web service anotada con la anotación @WebService (serviceName="MiServicio"), que extiende la clase de soporte SpringBeanAutowiringSupport, permitiendo inyectar un servicio a través de la anotación @AutoWired:

@WebService(serviceName="MiServicio") 
        public class AccountServiceEndpoint extends SpringBeanAutowiringSupport { 
            @Autowired 
            private RegionService regionService; 
         
            @WebMethod 
            public void insertRegion(Region region) { 
                biz.insertAccount(acc); 
            } 
         
            @WebMethod 
            public Regions[] getRegions(String nombre) { 
                return regionService.getAccounts(name); 
            } 
        } 

b. Servidor integrado

El JDK de Oracle soporta la exposición de servicios Web JAX-WS usando el servidor HTTP integrado. SimpleJaxWsServiceExporter de Spring detecta todos los beans anotados @WebService en el contexto de la aplicación Spring y los exporta a través del servidor JAX-WS predeterminado (el servidor HTTP JDK). Los endpoints se definen y administran directamente como beans Spring.

<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter"> 
            <property name="baseAddress" value="http://localhost:8080/"/> 
        </bean> 
         
        <bean id="regionServiceEndpoint" class="ejemplos.RegionServiceEndpoint"> 
            ... 
        </bean> 

RegionServiceEndpoint puede derivar de SpringBeanAutowiring Support de Spring, pero esto no es obligatorio:

@WebService(serviceName="RegionService") 
        public class RegionServiceEndpoint {  
          
            @Autowired 
            private RegionService regionService; 
         
            @WebMethod 
            public void insertRegion(Region region) { 
                regionService.insertRegion(region); 
            } 
            @WebMethod 
            public List<Region> getRegionss(String nombre) { 
                return regionService.getRegions(nombre); 
            } 
        } 

Spring permite utilizar un servicio web externo, usando JAX-WS.

Spring proporciona dos factorys de beans para crear proxys de servicio web JAX-WS, a saber: LocalJaxWsServiceFactoryBean y JaxWsPortProxyFactoryBean.

LocalJaxWsServiceFactoryBean es un FactoryBean para las referencias de servicio JAX-WS definidas localmente. Utiliza las instalaciones subyacentes de LocalJaxWsServiceFactory.

Las referencias del servicio JAX-WS también se pueden buscar en el entorno JNDI del contenedor Jakarta EE.

JaxWsPortProxyFactoryBean es un FactoryBean para un puerto específico de un servicio JAX-WS. Expone un proxy para el puerto, que se utilizará para referencias de bean y hereda las propiedades de configuración de un JaxWsPortClientInterceptor.

<bean id="regionWebService" 
        class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean"> 
            <property name="serviceInterface" value="ejemplos.RegionService"/> 
            <property name="wsdlDocumentUrl" 
        value="http://localhost:8888/RegionServiceEndpoint?WSDL"/> 
            <property name="namespaceUri" value="https://ejemplos/"/> 
            <property name="serviceName" value="RegionService"/> 
            <property name="portName" value="RegionServiceEndpointPort"/> 
        </bean> 

Propiedades de JaxWsPortProxyFactoryBean:

Variables

Explicaciones

serviceInterface

La interfaz que utilizan los clientes.

wsdlDocumentUrl

La dirección URL del archivo WSDL.

namespaceUri

Corresponde a targetNamespace en el archivo .wsdl.

serviceName

Corresponde al nombre del servicio en el archivo .wsdl.

portName

Corresponde al nombre del puerto en el archivo .wsdl.

Declaración del bean:

<bean id="cliente" class="ejemplos.RegionClienteImpl"> 
            ... 
            <property name="servicio" ref="regionWebService"/> 
        </bean> 

Implementación:

public class RegionClienteImpl { 
            private RegionService service; 
            public void setService(RegionService service) { 
                this.service = service; 
            } 
            public void inserer() { 
                service.insertRegion(...); 
            } 
        } 

3. Los EJB

En la mayoría de los casos, es más fácil sustituir los EJB por singletons Spring (@Service). Sin embargo, esto último no impide el uso de los EJB, sino que incluso facilita su acceso y sus implementaciones, ya sean variantes de EJB local, EJB remoto o POJO.

En sí mismo, EJB es bastante sencillo, pero su configuración y uso en un servidor Java EE o Jakarta EE es complejo. En nuestros ejemplos para este capítulo, utilizamos un servidor Wildfly en la versión wildfly-26.0.1.Final. Se compone de un EAR que contiene un WAR. La aplicación Spring Boot está en este WAR. Utiliza los EJB contenidos en otro EAR.

El ejemplo proporcionado en los archivos descargables permite experimentar y ampliar el estudio de este asunto.

a. Funcionamiento

He aquí un breve recordatorio de cómo funcionan los EJB modernos.

Los EJB son Enterprise JavaBeans. Se utilizan para desarrollar componentes evolutivos, escalables, distribuidos y del lado del servidor que normalmente integran la lógica de negocio de la aplicación.

Hay varias categorías de EJB:

  • EJB que representan datos: Entity EJB (obsoletos).

  • EJB que ofrecen servicios con o sin conservación de estado entre llamadas: EJB de sesión.

  • EJB que realizan tareas asíncronas: EJB Message (MDB).

Estos EJB pueden evolucionar en un contexto transaccional.

Hay varias versiones de EJB:

Desde la versión 1 hasta la versión 2.1, van acompañados de descriptores de despliegue en XML que guían al servidor e indican la configuración de la transaccionalidad.

Desde la versión 3, las anotaciones se han utilizado mucho. Ya no hay necesidad de XML.

Las entidades EJB que ya no se utilizan hoy en día son de dos tipos:

  • BMP (Bean Managed Persistence)

  • CMP (Container Managed Persistence)

Desde la versión 3, el concepto de BMP/CMP ya no existe. Los EJB utilizan ORM (mapping objet-relationnel) a través de JPA en forma de anotaciones o configuración XML.

El cliente accede a un EJB a través de una llamada RMI (o RMI-IIOP u otras) especificando un nombre JNDI. A continuación, puede llamar a uno o varios métodos en el objeto como si fuera local. Existe un mecanismo de transferencia de excepciones producidas en el objeto servidor hacia el cliente.

En el resto del capítulo, solo tratamos con los EJB «modernos» basados en anotaciones, pero Spring también funcionaría muy bien con EJB más antiguos. Los EJB messages o MDB (Message Driven Bean) tampoco se procesan porque son demasiado específicos y un poco anticuados para los MoM (Message-oriented Middleware) modernos.

Los EJB vienen en dos variantes: con estado (@Stateful) y sin estado (@Stateless).

@Stateless 
        public class StatelessSessionBeanImpl implements StatelessSessionBean { 
         
            public String decirHola() { 
                return ("Hola"); 
            } 
        } 

@Stateful 
        public class StatefulSessionBeanImpl implements StatefulSessionBean { 
         
            private String usuario; 
         
            public void login(String usuario) { 
                this.usuario = usuario; 
            } 
         
            public String decirHola() { 
                if (usuario == null) { 
                    return ("Hola"); 
                } 
                return ("Hola " + usuario + " !"); 
            } 
        } 

Para ver los ejemplos, descargue e instale WildFly en la versión wildfly-26.0.1.Final como se muestra en la dirección https://docs.wildfly.org/26/Getting_Started_Guide.html#installation. Debe configurar la variable de entorno JBOSS_HOME para que apunte al directorio de instalación de WildFly, y JAVA_HOME, al JDK.

Para iniciar el servidor, ejecute el script standalone.bat o .sh desde el directorio bin.

También debe modificar su settings.xml Maven para añadir los plugins de WildFly:

<pluginGroups> 
         [...] 
         <pluginGroup>org.wildfly.plugins</pluginGroup> 
         </pluginGroups> 

Esto permite ejecuta un mvn wildfly:deploy.

Para usar los EJB 3.2 con WildFly, necesitamos establecer las siguientes dependencias:

<dependency> 
            <groupId>javax</groupId> 
            <artifactId>javaee-api</artifactId>  
            <version>8.0.1</version> 
            <scope>provided</scope> 
        </dependency>  
          
        <dependency> 
            <groupId>org.wildfly.bom</groupId> 
            <artifactId>wildfly-jakartaee8-with-tools-builder</artifactId> 
            <version>20.0.1.Final</version> 
            <scope>import</scope> 
            <type>pom</type> 
        </dependency> 

Las BOM se explican en la dirección https://github.com/wildfly/boms

La interfaz de negocio del lado del cliente puede ser local o remota, a través de anotaciones @Local y @Remote. Un bean anotado @Local solo es accesible si está en la misma aplicación que el bean al que llama. El bean debe estar en el mismo .ear o .war. Se puede acceder a un bean anotado @Remote desde una aplicación remota. A diferencia de lo que sucede con los microservicios que se comunican a través de HTTP, la comunicación entre EJB se realiza a través de un protocolo dedicado interno.

b. Creación del EJB remoto

Primero creamos la interfaz del bean y la llamamos Hola:

@Remote 
         public interface Hola { 
         String getHola(); 
        } 

Ahora implementaremos la interfaz anterior y llamaremos a la implementación concreta HolaBean:

@Stateless(name = "Hola") 
        public class HolaBean implements Hola { 
         
            @Resource 
            private SessionContext context; 
         
            @Override 
            public String getHola() { 
                return "Hola a ti"; 
            } 
        } 

En la medida de lo posible, intentamos estar en modo sin estado (Stateless) para no tener que propagar el estado entre diferentes instancias en diferentes servidores. Esta noción es central porque los EJB se han podido colocar en un estado de espera y el estado se puede volver inconsistente al reactivarlos (activación/desactivación de los EJB).

La anotación @Resource inyecta el contexto de la sesión en el bean remoto.

La interfaz SessionContext permite acceder al contexto de sesión de ejecución. El contenedor proporciona esta sesión después de crear la instancia. Los EJB sin estado se gestionan en un pool de objetos beans sin estado.

La conservación de los valores de las variables de instancia no está garantizada durante las llamadas al método de búsqueda, pero es posible inicializarlos cuando el objeto se construye a través de un método anotado con @PostConstruct.

La siguiente configuración del plugin se utiliza para configurar el JAR de destino para el bean:

<plugin> 
            <artifactId>maven-ejb-plugin</artifactId> 
            <version>2.4</version> 
            <configuration> 
                <ejbVersion>3.2</ejbVersion> 
            </configuration> 
        </plugin> 

Para desplegar el bean en un servidor WildFly, es necesario:

Empaquetado: mvn package 
        Despliegue: mvn wildfly:deploy 

c. Configuración de Maven en el lado del cliente

Se deben añadir las siguientes dependencias:

<dependency> 
            <groupId>org.wildfly</groupId> 
            <artifactId>wildfly-ejb-client-bom</artifactId> 
            <type>pom</type> 
            <scope>import</scope> 
        </dependency> 
         
        <dependency> 
            <groupId>com.baeldung.ejb</groupId> 
            <artifactId>ejb-remote</artifactId> 
            <type>ejb</type> 
        </dependency> 

Para acceder al bean remoto, debe crear un archivo en src/main/resources/jboss-ejb-client.properties:

remote.connections=default 
        remote.connection.default.host=127.0.0.1 
        remote.connection.default.port=8080 
        remote.connection.default.connect.options.org.xnio.Options. 
        SASL_POLICY_NOANONYMOUS = false 
        remote.connection.default.connect.options.org.xnio.Options. 
        SASL_POLICY_NOPLAINTEXT = false 
        remote.connection.default.connect.options.org.xnio.Options. 
        SASL_DISALLOWED_MECHANISMS = ${host.auth:JBOSS-LOCAL-USER} 
        remote.connection.default.username=myusername 
        remote.connection.default.password=mypassword 

d. Creación del cliente

El nombre JNDI del bean remoto tiene la forma:

ejb:${appName}/${moduleName}/${distinctName}/ 
        ${beanName}!${viewClassName} 

Composición del nombre JNDI:

Variable

Significados

appName

Nombre de la aplicación para el despliegue. Aquí no usamos ningún archivo EAR, sino una simple implementación JAR o WAR, por lo que el nombre de la aplicación estará vacío.

moduleName

El nombre que establecimos para nuestro despliegue temprano, por lo que es ejb-remote.

distinctName

Un nombre específico que opcionalmente se puede asignar a los despliegues desplegados en el servidor.

beanName

Nombre de la clase de implementación EJB.

viewClassName

Nombre de la interfaz remota.

Carga del EJB a través de JNDI:

public Hola lookup() throws NamingException { 
            String appName = ""; 
            String moduleName = "remote"; 
            String distinctName = ""; 
            String beanName = "Hola"; 
            String viewClassName = Hola.class.getName(); 
            String toLookup = String.format("ejb:%s/%s/%s/%s!%s", 
              appName, moduleName, distinctName, beanName, viewClassName); 
            return (Hola) context.lookup(toLookup); 
        } 

Creación del contexto de sesión:

public void createInitialContext() throws NamingException { 
            Properties prop = new Properties(); 
            prop.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming"); 
            prop.put(Context.INITIAL_CONTEXT_FACTORY, 
              "org.jboss.naming.remote.client.InitialContextFacto[ERROR] 
            prop.put(Context.PROVIDER_URL, "http-remoting://127.0.0.1:8080"); 
            prop.put(Context.SECURITY_PRINCIPAL, "testUser"); 
            prop.put(Context.SECURITY_CREDENTIALS, "admin1234!"); 
            prop.put("jboss.naming.client.ejb.context", false); 
            context = new InitialContext(prop); 
        } 

El método lookup() permite cargar el EJB en el contexto creado por el método createInitialContext().

e. Integración adicional

Spring ofrece una integración más profunda para la administración de los beans de sesión sin estado (SLSB), con dos paquetes:

  • Paquete org.springframework.ejb.access:

Clase

Descripción

AbstractRemoteSlsb InvokerInterceptor

Clase base para interceptores que exigen beans de sesión Stateless remotos.

AbstractSlsbInvoker Interceptor

Clase base para interceptores que exigen la clase remota para los interceptores AOP que llaman a beans de sesión sin estado.

LocalSlsbInvokerInterceptor

Invocador para un bean de sesión local sin estado.

LocalStatelessSession ProxyFactoryBean

FactoryBean práctico para los proxys locales SLSB (Stateless Session Bean).

SimpleRemoteSlsbInvoker (interceptor)

Invocador básico para un bean de sesión sin estado remoto.

SimpleRemoteStateless SessionProxyFactoryBean

FactoryBean útil para los proxys SLSB remotos.

  • Paquete org.springframework.ejb.config:

Clase

Descripción

JeeNamespaceHandler

NamespaceHandler para el namespace ’jee’.

Hemos visto que, para llamar a un método en un bean de sesión sin estado (SLSB) local o remoto, el código de cliente normalmente tiene que realizar una búsqueda JNDI para obtener el objeto EJB Home (local o remoto) y, a continuación, usar una llamada de método create sobre ese objeto para obtener el objeto EJB real (local o remoto).

Para crear el componente bean sin estado, debe crear una interfaz remota y una clase de bean.

import javax.ejb.Remote; 
         
         
        @Remote 
        public interface OperationImplRemote int multiplier(int a,int b); 
        } 
         
        @Stateless(mappedName="st1"public class OperationImpl implements OperatonImplRemote { 
          public int multiplicar(int a,int b){ 
              return a*b; 
          } 
        } 
         
        public class Test { 
          public static void main(String[] args)throws Exception { 
            Context context=new InitialContext(); 
            AdderImplRemote remote=(AdderImplRemote)context.lookup("st1"); 
            System.out.println(remote.multiplicar(2,3)); 
          } 
        } 

La ejecución del método anterior muestra «6» como resultado.

Para evitar repetir código de bajo nivel, muchas aplicaciones EJB utilizan las plantillas Service Locator y Business Delegate. También son desing patterns. Es mejor usarlas que utilizar búsquedas JNDI en el código cliente, pero sus implementaciones habituales tienen inconvenientes importantes.

f. El design pattern Business Delegate

Se trata de utilizar facades de negocio en los clientes para el intercambio de datos en forma de objetos de transferencia. La salida de la capa cliente se puede encapsular en un Business Delegate, lo que permite reducir el acoplamiento entre la capa de negocio y la de presentación.

Implementamos una clase business delegate por facade de negocio. Esta clase se hace cargo de los métodos de negocio y oculta aspectos remotos. También maneja excepciones de bajo nivel.

A continuación, utilizamos un Service Locator para gestionar las búsquedas JNDI.

Los intercambios entre capas se realizan con POJO.

Spring proporciona tres niveles de simplificación: modo simple, modo elaborado y modo integrado.

Modo simple

En el modo simple, Spring proporciona asistencia para el diseño de Business Delegate.

SimpleRemoteStatelessSessionProxyFactoryBean se puede utilizar para cargar los beans de acceso a los EJB a través de un Service Locator. Se toman los métodos de EJB, pero sin las RemoteException.

El delegado:

public interface HolaDelegate { 
         HolaTO hola(String nombre); 
        } 

Configuración del bean en el archivo ejb-client.xml:

<bean id="holaDelegateSimple" lazy-init="true" 
             class="org.springframework.ejb.access. 
        SimpleRemoteStatelessSessionProxyFactoryBean"> 
         
           <property name="jndiName" value="ejb/Hello" /> 
           <property name="businessInterface" 
                     value="fr.eni.HelloDelegate" /> 
        </bean>  

De esta manera, es posible acceder al EJB de una forma muy flexible, a través del contexto Spring:

final ApplicationContext ctx ; 
        ctx = new ClassPathXmlApplicationContext(new String[] 
        {"ejb-client.xml"}); 
         
        HolaDelegate del = 
        (HolaDelegate)ctx.getBean("holaDelegateSimple"); 
        HolaTO hi = del.hola("tú")); 

Business Delegate elaborado

Usamos AOP para interceptar los métodos para capturar excepciones ServerException

public class ExceptionHandlingInterceptor implements MethodInterceptor { 
           public Object invoke(MethodInvocation invocation) throws Throwable { 
               try { 
                   return invocation.proceed(); 
               } catch (ServerException e) { 
                   throw new BusinessDelegateException( 
                           "Problema de conexión al servidor", e.getCause()); 
               } catch (Exception e) { 
                   throw new BusinessDelegateException(e); 
               } 
           } 
        } 

Luego usamos un proxy que implementa la interfaz de delegación:

<bean id="holaDelegate" 
              class="org.springframework.aop.framework.ProxyFactoryBean"> 
            <property name="proxyInterfaces"> 
                <value>springdelegate.BonjourDelegate</value> 
            </property> 
            <property name="target"> 
                <ref local="holaDelegateSimple"/> 
            </property> 
            <property name="interceptorNames"> 
               <list> 
                   <value>exception</value> 
               </list> 
            </property> 
        </bean> 

Por lo tanto, podemos tener varios interceptores mediante el uso de AOP Spring (pointcuts, advisors, etc.).

Lo que da en el lado del cliente:

final ApplicationContext ctx; 
        ctx = new ClassPathXmlApplicationContext(new String[] 
        {"ejb-client.xml"}); 
        HolaDelegate del = (HelloDelegate)ctx.getBean("holaDelegate"); 
        HolaTO hi = del.hola("tu")); 

Modo integrado

Spring proporciona un modo integrado que facilita las pruebas.

Para un componente:

public interface MyComponent { 
         ... 
        } 

En un bean de Spring, como un controlador:

private MyComponent myComponent; 
         
        public void setMyComponent(MyComponent myComponent) { 
            this.myComponent = myComponent; 
        } 

Usando un LocalStatelessSessionProxyFactoryBean, que es un objeto proxy EJB:

<bean id = "myComponent" 
                class"org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"> 
            <property name = "jndiName" value = "ejb / myBean" /> 
            <property name = "businessInterface" value = "com.mycom.MyComponent" /> 
        </bean> 
        <bean id = "myController" class = "com.mycom.myController"> 
            <property name = "myComponent" ref = "myComponent" /> 
        </bean> 

Spring gestiona con AOP la inyección del EJB y su puesta en marcha con manejo de excepciones.

La definición del bean myController define la propiedad myComponent de la clase de controlador en el proxy EJB.

La configuración XML se puede simplificar a través del elemento de configuración <jee: local-slsb> en el espacio de nombres jee de Spring:

<jee: local-slsb id = "myComponent" jndi-name = "ejb / myBean" 
                business-interface = "com.mycom.MyComponent" /> 
         
        <bean id = "myController" class = "com.mycom.myController"> 
            <property name = "myComponent" ref = "myComponent" /> 
        </bean> 

También hay un LocalSlsbInvokerInterceptor que permite añadir comportamientos con el AOP.

Para acceder a los SLSB remotos, utilice el elemento de configuración SimpleRemoteStatelessSessionProxyFactoryBean o <jee: remote-slsb>.

Para Session Beans EJB 3, también podemos usar un Jndi-ObjectFactoryBean / <jee: jndi-lookup>, ya que las referencias de componentes son completamente utilizables. Se exponen para las búsquedas JNDI sencillas. La definición de búsquedas <jee: local-slsb> o <jee: remote-slsb> explícitas proporciona una configuración de acceso EJB coherente y más explícita.

Respecto a las pruebas, podemos usar Arquillian o un JBoss embedded (integrado). Hay un plugin Spring Arquillian, pero no funciona bien con las últimas versiones.

Puntos clave

  • El framework es configurable en Java o mediante archivos XML.

  • Preferiblemente, usaremos la configuración de Java para nuevos proyectos.

  • Los beans se inyectan gracias a los constructores o por los setters.

  • Podemos mejorar el sistema de log predeterminado.

  • Es muy fácil hacer pruebas con Spring.

  • Es posible utilizar conjuntamente Spring y EJB.

Introducción

En este capítulo abordaremos los elementos más complejos de Spring. Comenzaremos por los recursos, continuaremos con los conversores y formateadores y terminaremos con los binders y validadores.

Estos elementos se pueden utilizar en los contextos de aplicación standalone y aplicación web.

Archivos de recursos Archivos de recursos

Un recurso es un archivo de texto, por ejemplo. Primero veremos el uso normal de los archivos de recursos en Java. Seguidamente, veremos cómo cargar un archivo de recursos con Spring usando el contexto Spring. Para terminar, veremos cómo utilizar el ResourceLoaderAware, que es una interfaz que debe implementar cualquier objeto que desee ser notificado por el ResourceLoader (generalmente, ApplicationContext) en el que se ejecuta.

1. Archivos de recursos estándares

Los archivos y las URL que hacen referencia a un archivo se pueden leer de manera estándar, como en el siguiente ejemplo:

@Log 
        public class Recursos1 { 
           public static void procesarRecurso(Resource resource) { 
              try { 
                 InputStream is = resource.getInputStream(); 
                 BufferedReader br = new BufferedReader(new 
        InputStreamReader(is)); 
                 String linea; 
                 while ((linea = br.readLine()) != null) { 
                    log.info(linea); 
                 } 
                 br.close(); 
              } catch (IOException e) { 
                 e.printStackTrace(); 
              } 
           } 
           public static void main(String[] args) throws IOException { 
              Resource r0 = new FileSystemResource("src/main/resources/fr/ 
        eni/editions/recurso.txt"); 
              procesarRecurso(r0); 
         
              String 
        archivo=System.getProperty("user.dir")+"/src/main/resources/fr/ 
        eni/editions/recurso.txt"; 
              log.info("Archivo:"+archivo); 
         
              Resource r1 = new UrlResource("file:"+archivo); 
              procesarRecurso(r1); 
              Resource r2 = new ClassPathResource("fr/eni/editions/ 
        recurso.txt"); 
              procesarRecurso(r2); 
           } 

Tenemos un método processResource en la clase Recursos1 con el argumento resource de tipo Resource. Este método procesa el recurso y se retomará en los ejemplos siguientes.

La primera parte abre el archivo del ejemplo con un FileSystemResource y lo procesa con el método procesarRecurso.

La segunda parte del ejemplo abre el archivo usando un UrlResource y, a continuación, lo procesa con el método procesarRecurso.

Por lo tanto, el primero es un archivo, y el segundo, una URL que apunta a un archivo.

La clase UrlResource también permite actuar sobre recursos de otros tipos, como flujos HTTP, etc.

2. Archivos de recursos cargados con el contexto Spring

Podemos utilizar las facilidades de Spring para cargar recursos.

Reutilizamos el procesamiento de recursos del primer ejemplo.

@Log 
        public class Recursos2 { 
           public static void procesarRecurso(Resource resource) { 
         [...] 
           } 
           public static void main(String[] args) { 
              ApplicationContext ctx = new ClassPathXmlApplicationContext( 
                    new String[] { "applicationContext.xml" }); 
              String rep=System.getProperty("user.dir"); 
              log.info("Rep actual:"+rep); 
         
              Resource r0 = ctx 
         
        .getResource("file:"+rep+"/src/main/resources/fr/eni/editions/ 
        recurso.txt"); 
              procesarRecurso(r0); 
              Resource r1 = ctx 
         
        .getResource("classpath:fr/eni/editions/recurso.txt"); 
              procesarRecurso(r1); 
              Resource r2 = ctx 
         
        .getResource("url:http://echo.jsontest.com/key/value/one/two"); 
              procesarRecurso(r2); 
         
           } 

El método getResource de ApplicationContext permite cargar cualquier tipo de recurso especificando el protocolo File o HTTP. Utilizamos el mismo método, independientemente del tipo de recurso el código es más sencillo. Es como una facade que enmascara la complejidad subyacente.

3. Archivos de recursos cargados con un servicio ResourceLoaderAware

Podemos usar las capacidades de Spring para cargar los recursos y exponerlos en un servicio. A continuación, el servicio que implementa ResourceLoaderAware expone dos métodos:

public void setResourceLoader(ResourceLoader resourceLoader) 

y

public Resource getResource(String location) 

Spring inyecta automáticamente el recurso en el bean y lo hace disponible con el método getResource.

Reutilizamos el procesamiento de recursos del primer ejemplo para procesar el recurso.

a. El programa principal (main)

@Log 
        public class Recursos3 { 
         
           public static void procesarRecurso(Resource resource) { 
        [...] 
           } 
         
           public static void main(String[] args) { 
              ApplicationContext ctx = new ClassPathXmlApplicationContext( 
                    new String[] { "applicationContext.xml" });  
              RecursoHelperService svc = 
        (RecursoHelperService)ctx.getBean("recursoHelperService"); 
         
         
              String rep=System.getProperty("user.dir"); 
              log.info("Rep actual:"+rep); 
         
              Resource r0 = svc.getResource("file:"+rep+"/src/main/resources/ 
        fr/eni/editions/recurso.txt"); 
              procesarRecurso(r0); 
              Resource r1 = svc 
         
        .getResource("classpath:fr/eni/editions/recurso.txt"); 
              procesarRecurso(r1); 
              Resource r2 = svc 
         
        .getResource("url:http://echo.jsontest.com/key/value/one/two"); 
              procesarRecurso(r2); 
           } 

La creación del bean por la factory Spring activará la carga automática del recurso.

El sitio web http://echo.jsontest.com es muy práctico para experimentar con JSON.

b. El servicio ResourceLoaderAware

public class RecursoHelperService implements ResourceLoaderAware 
           private ResourceLoader resourceLoader; 
           public void setResourceLoader(ResourceLoader resourceLoader) { 
              this.resourceLoader = resourceLoader; 
           } 
           public Resource getResource(String location){ 
              return resourceLoader.getResource(location); 
           } 

Este servicio Spring permite centralizar la carga del recurso en un singleton. Solo se carga una vez cuando se inicia la aplicación.

Conversores y formateadores Conversor Formateador

Spring a menudo manipula los valores usando cadenas de caracteres. Este es el caso cuando tenemos una configuración en un archivo de texto; por ejemplo, un archivo XML. Este principio también se utiliza cuando Spring intercambia datos entre las diferentes partes de una aplicación, como en el caso de Spring MVC, para el que los datos hacia y desde las vistas, ya sean páginas JSP o páginas JSF, circulan en formato de texto.

Un conversor puede transformar un objeto en String, pero también un objeto de una clase a otra.

1. Built-in converters

Un built-in converter es un conversor integrado. Usamos la clase Spring GenericConversionService para tener los conversores estándar. Esta clase permite disponer de un servicio de conversión que se puede extender con conversores personalizados.

En el siguiente ejemplo, se muestra cómo utilizar un DefaultConversionService que implementa la clase de uso general GenericConversionService.

Declaración del conversor genérico

GenericConversionService cs = new DefaultConversionService(); 

a. Para los tipos estándares

Para los tipos estándares, utilizaremos los conversores disponibles.

En el siguiente ejemplo se presenta la conversión, seguida de la prueba unitaria asociada:

Boolean buleano = cs.convert("true", Boolean.class); 
        assertTrue(buleano); 
         
        buleano = cs.convert("no", Boolean.class); 
        assertFalse(buleano); 
         
        Caracter car = null; 
        car = cs.convert("Z", Caracter.class); 
        assertEquals(new Caracter('Z'), car); 
         
        try { 
           car = cs.convert("Exception", Caracter.class); 
           fail("Debe generar una excepción porque la cadena no es un 
        carácter"); 
        } catch (ConversionFailedException aExp) { 
        assert (aExp.getMessage() 
        .contains("Failed to convert from type java.lang.String to type 
        java.lang.Caracter")); 
        } 
        Integer entero = cs.convert("43210", Integer.class); 
        assertEquals(new Integer(43210), entero); 
         
        Float flotante = cs.convert("123.456f", Float.class); 
        assertEquals(new Float(123.456f), flotante); 

b. Para tablas y listas

Para las tablas y las listas, podemos utilizar directamente String[].class o List.class en el argumento Class<T> targetType del conversor:

String[] tabCadena = cs.convert("Redondo,Cuadrado,Triangular", 
                               String[].class); 
        assertEquals("Redondo", tabCadena[0]); 
        assertEquals("Cuadrado", tabCadena[1]); 
        assertEquals("Triangular", tabCadena[2]); 
         
        List<String> lstCadena = cs.convert( 
                   "Redondo,Cuadrado,Triangular", List.class); 
        assertEquals("Redondo", lstCadena.get(0)); 
        assertEquals("Cuadrado", lstCadena.get(1)); 
        assertEquals("Triangular", lstCadena.get(2)); 

También podemos utilizar una tabla de cadenas que da el mismo resultado:

lstCadena = conversionService.convert( 
                  new String[]{"Redondo","Cuadrado","Triangular"}, List.class); 

La conversión es reversible:

String formas = cs.convert( 
                  new String[]{"Redondo","Cuadrado","Triangular"}, String.class); 
        assertEquals("Redondo,Cuadrado,Triangular", formas); 

c. Para las enumeraciones

Para la siguiente enumeración:

 public enum MiEnum { 
            UNO, 
            DOS, 
            TRES 
        } 

tenemos:

MiEnum miEnum = conversionService.convert("DOS", MiEnum.class); 
        assertEquals(miEnum, MiEnum.DOS); 

d. Para objetos en general

Si tenemos un usuario:

public class Usuario { 
        [accesseurs] 
           private String nom; 
           private String apellido; 
        } 

podemos fabricar un conversor que convierta nuestro objeto en un String y viceversa:

public class StringToUsuarioConverter implements 
        Converter<String, Usuario>{ 
           @Override 
           public Usuario convert(String UsuarioAsString) { 
              if (UsuarioAsString == null){ 
                 throw new 
        ConversionFailedException(TypeDescriptor.valueOf(String.class), 
                    TypeDescriptor.valueOf(String.class), 
        UsuarioAsString, null); 
              } 
              String[] tempArray = UsuarioAsString.split(","); 
              Usuario article = new Usuario(Integer.parseInt 
        (tempArray[0]), tempArray[1], tempArray[2]); 
              return article; 
           } 
        } 

y:

public class UsuarioToStringConverter implements 
              Converter<Usuario, String> { 
           @Override 
           public String convert(Usuario article) { 
         
              if (article == null) { 
                 throw new ConversionFailedException( 
                       TypeDescriptor.valueOf(Usuario.class), 
                       TypeDescriptor.valueOf(String.class), article, null); 
              } 
              StringBuilder builder = new StringBuilder(); 
              builder.append(article.getId()); 
              builder.append(","); 
              builder.append(article.getNombre()); 
              builder.append(","); 
              builder.append(article.getApellido()); 
              return builder.toString(); 
           } 
        } 

El conversor debe estar registrado para usarlo:

private static void testToObject(GenericConversionService 
        conversionService) { 
           conversionService.addConverter(new UsuarioToStringConverter()); 
           Usuario usuObject = new Usuario(2, "CONNOR","John"); 
           String usuAsString = conversionService.convert( 
                 new Usuario[] { usuObject }, String.class); 
           System.out.println("Usuario -->" + usuAsString); 
        } 

2. Convertir un Array en Collection y String Collection String

Es posible convertir directamente un Array en Collection y en String, como se muestra en el siguiente ejemplo:

@Log 
        public class LasConversiones2 { 
           public static void main(String[] args) { 
              GenericConversionService conversionService = 
        ConversionServiceFactory.createDefaultConversionService(); 
              testToCollection(conversionService); 
              testToString(conversionService); 
              testToObject(conversionService); 
           } 
         
           private static void testToCollection (GenericConversionService 
        conversionService){ 
              @SuppressWarnings("unchecked") 
              List<String> listOfTerm = conversionService.convert( 
                 new String[]{"t100","t800","tx"}, List.class); 
              for (String term : listOfTerm){ 
                 System.out.println("Terminator is " + term); 
              } 
           } 
         
           private static void testToString(GenericConversionService  
        conversionService){ 
         
              String terms = conversionService.convert( 
                 new String[]{"t100","t800","tx"}, String.class); 
              System.out.println("Terminator is " + terms); 
           } 
         
           private static void testToObject(GenericConversionService  
        conversionService){ 
         
              conversionService.addConverter(new  
        StringToUsuarioConverter()); 
         
              Usuario usuario = conversionService.convert( 
                 new String[]{"5,John,REESE"}, Usuario.class); 
              System.out.println("El nombre es " + usuario.getNombre()); 
              System.out.println("El apellido es " + usuario.getApellido()); 
           } 
        } Built In For Collection Type Test 

3. Converter Factory Factory

También podemos utilizar factories que nos devolverán conversores.

a. Clase StringToUsuarioConverterFactory

Por ejemplo, podemos tener una factory para el conversor para la clase Usuario

public class StringToUsuarioConverterFactory implements 
              ConverterFactory<String, Usuario> { 
           @SuppressWarnings("unchecked") 
           @Override 
           public <T extends Usuario> Converter<String, T>  
        getConverter( 
                 Class<T> arg0) { 
              return (Converter<String, T>) new  
        StringToUsuarioConverter(); 
           } 

b. Clase UsuarioToStringConverterFactory

Y viceversa:

public class UsuarioToStringConverterFactory implements 
              ConverterFactory<Usuario, String> { 
           @Override 
           public <T extends String> Converter<Usuario, T>  
        getConverter( 
                 Class<T> arg0) { 
              return (Converter<Usuario, T>) new  
        UsuarioToStringConverter(); 
           } 

c. Clase LasConversionesConFactories

En el siguiente ejemplo se muestra el uso de factories de conversores:

public class LasConversionesConFactories { 
           public static void main(String[] args) { 
              GenericConversionService conversionService = new  
        GenericConversionService(); 
              conversionService 
                    .addConverterFactory(new  
        UsuarioToStringConverterFactory()); 
              conversionService 
                    .addConverterFactory(new  
        StringToUsuarioConverterFactory()); 
              String usuarioAsString = "7,Sarah,CONNOR"; 
              Usuario usuario =  
        conversionService.convert(usuarioAsString,  
                    Usuario.class); 
         
              System.out.println("El nombre es " + usuario.getNombre()); 
              System.out.println("El apellido es " + usuario.getApellido()); 
              usuarioAsString =  
        conversionService.convert(usuario, String.class); 
              System.out.println("Se utiliza como cadena [" + 
        usuarioAsString + "]"); 
           } 

4. Los formateadores predeterminados

Hay una variedad de formateadores disponibles de forma nativa en Spring.

El siguiente ejemplo ilustra cómo se utilizan.

a. Clase LasConversionesFormateadoras

Los formateadores se pueden usar para fechas y números, como se muestra en el siguiente ejemplo:

@Log 
        public class LasConversionesFormateadores { 
           public static void main(String[] args) throws Exception { 
              testDateFormatter(); 
              testNumberFormatter(); 
           } 
           private static void testDateFormatter() { 
              Formatter dateFormatter = new DateFormatter(); 
              String dateAsString = dateFormatter.print(new Date(),  
        Locale.SPAIN); 
              System.out.println("Fecha en España: " + dateAsString); 
           } 
           private static void testNumberFormatter() throws Exception { 
              NumberFormatter doubleFormatter = new NumberFormatter(); 
              doubleFormatter.setPattern("#####.###"); 
              String number = doubleFormatter.print(new Double(12345.6789d), 
                    Locale.SPAIN); 
              System.out.println("Número: " + number); 
           } 

5. Formateadores personalizados Formateador

Podemos fabricar formateadores personalizados. Como ejemplo, vamos a utilizar el formateador de números de tarjetas de crédito. Este ejemplo es bastante común, pero ilustra las posibilidades de formato.

a. Clase TarjetaDeCredito

Considere una tarjeta de crédito que consta de cuatro tuplas de cuatro números cada una:

@Data 
        @AllArgsConstructor 
        @NoArgsConstructor 
        public class TarjetaDeCredito { 
           private int primeraTupla; 
           private int segundaTupla; 
           private int terceraTupla; 
           private int cuartaTupla; 
        } 

b. Clase TarjetaDeCreditoParser

Este es un ejemplo de un parseador que desglosa los componentes de un número de tarjeta de crédito:

public class TarjetaDeCreditoParser implements Parser { 
           @Override 
           public TarjetaDeCredito parse(String cc, Locale locale) 
                 throws ParseException { 
              String caracteres[] = cc.split("-"); 
              if (caracteres == null || caracteres.length != 4) { 
                 throw new org.springframework.expression.ParseException(-1, 
                       "Invalid format"); 
              } 
              TarjetaDeCredito c = new TarjetaDeCredito(); 
              c.setPrimeraTupla(Integer.parseInt(caracteres[0])); 
              c.setSegundaTupla(Integer.parseInt(caracteres[1])); 
              c.setTerceraTupla(Integer.parseInt(caracteres[2])); 
              c.setCuartaTupla(Integer.parseInt(caracteres[3])); 
              return c; 
           } 
        } 

c. Clase TarjetaDeCreditoPrinter

He aquí el código de un visor simplificado que muestra el número de tarjeta de crédito. El uso de la librería Project Lombok simplifica el formateado gracias a la anotación @Data en la clase que proporciona el método toString mejorado.

public class TarjetaDeCreditoPrinter implements Printer { 
         
           @Override 
           public String print(Object cc, Locale locale) { 
              TarjetaDeCredito objet=(TarjetaDeCredito)cc; 
         
              return objet.toString(); 
           } 
        } 

d. Clase TarjetaDeCreditoFormatter

Un formateador es tanto un Printer como un Parser. A continuación, es necesario proporcionar las dos implementaciones de los métodos en cuestión, como en el siguiente ejemplo:

public class TarjetaDeCreditoFormatter implements Formatter { 
           private Parser parser; 
           private Printer printer; 
           public TarjetaDeCreditoFormatter(Parser parser, Printer printer) 
        { 
              this.parser = parser; 
              this.printer = printer; 
           } 
           @Override 
           public TarjetaDeCredito parse(String cc, Locale l) 
                 throws ParseException { 
              return (TarjetaDeCredito) parser.parse(cc, l); 
           } 
           @Override 
           public String print(Object cc, Locale l) { 
              return printer.print(cc, l); 
           } 
        } 

e. Clase LasConversionesFormateadoresEx2

En el ejemplo siguiente se muestra cómo utilizar formateadores y conversores:

@Log 
        public class LasConversionesFormateadoresEx2 { 
           public static void main(String[] args) { 
              FormattingConversionService service = new  
        FormattingConversionService(); 
              TarjetaDeCreditoParser parser = new TarjetaDeCreditoParser(); 
              TarjetaDeCreditoPrinter printer = new TarjetaDeCreditoPrinter(); 
              service.addFormatterForFieldType(TarjetaDeCredito.class,  
        printer, parser); 
              test1(service); 
              test2(service); 
           } 
         
           private static void test1(FormattingConversionService service) 
        { 
              String cc = "1111-2222-3333-4444"; 
              TarjetaDeCredito o = (TarjetaDeCredito) service.convert( 
                    cc, TarjetaDeCredito.class); 
              log.info(o.toString()); 
           } 
         
           private static void test2(FormattingConversionService service) 
        { 
              TarjetaDeCredito o = new TarjetaDeCredito(1111, 2222, 3333, 4444); 
              String cc = service.convert(o, String.class); 
              log.info("El número de la tarjeta de crédito es:" + cc); 
           } 
        } 

BeanWrappers, binding y validadores BeanWrappers Binding Wrappers

Los wrappers, el binding y la validación se utilizan juntas y es muy habitual en las aplicaciones Spring MVC.

1. Clase LosBeanWrappers

En este ejemplo se muestran las posibilidades de la clase BeanWrapperImpl:

@Log 
        public class LosBeanWrappers { 
           public static void main(String[] args) {  
              //Utilización del BeanWrapper 
              Usuario usuario = new Usuario(); 
                BeanWrapper bw = new BeanWrapperImpl(usuario); 
                bw.setPropertyValue("nombre", "John"); 
                bw.setPropertyValue("apellido", "DOE"); 
                bw.setPropertyValue("id", "200"); 
                final int id=200; 
                log.info("Verificación:"+(id==usuario.getId())+  
        "Usuario="+usuario); 
                try { 
                    bw.setPropertyValue("id", "no es numérico"); 
                } catch (TypeMismatchException e) { 
                   log.info("Err binding :"+e.getMessage()); 
                }  
         
                //Un poco más complejo 
                log.info("MutablePropertyValues:"); 
                MutablePropertyValues values = new MutablePropertyValues(); 
                values.addPropertyValue("nombre", "DOE"); 
                values.addPropertyValue("apellido", "Jane"); 
                //values.addPropertyValue("id", "2100"); 
                DataBinder dataBinder = new DataBinder(usuario, "John"); 
                dataBinder.setAllowedFields(new String [] {"nombre", "apellido"}); 
                dataBinder.setValidator(new UsuarioValidator()); 
                dataBinder.bind(values); 
                dataBinder.validate(); 
                log.info("allErrors:"); 
                List<ObjectError> allErrors =  
        dataBinder.getBindingResult().getAllErrors(); 
                for (Object object : allErrors) { 
                    ObjectError error = (ObjectError) object; 
                    log.info(error.toString()); 
                } 
         
           } 

2. Clase UsuarioValidador Validador

Los validadores se utilizan para comprobar la validez de los datos.

public class UsuarioValidador implements Validator { 
            public boolean supports(Class clazz) { 
                return Usuario.class.equals(clazz); 
            } 
            public void validate(Object obj, Errors e) { 
                ValidationUtils.rejectIfEmpty(e, "nombre", "nombre.vacio"); 
                Usuario p = (Usuario) obj; 
                if (p.getNombre().contains("s")) { 
                    e.rejectValue("nombre", "contiene.s"); 
                } else if (p.getId() < 0) { 
                    e.rejectValue("id", "id.neg"); 
                } 
            } 
VM6883:64

Puntos clave

  • Los conversores, binders, BeanWrappers y validadores son fáciles de usar con Spring.

  • Es necesario pensar en términos globales para reutilizar estas herramientas de conversión tanto como sea posible.

  • Es necesario utilizar las posibilidades de externalizar valores en archivos .properties para personalizar la configuración de Spring en función de elementos externos.

Introducción Programación orientada a aspectos AOP

La AOP (programación orientada a aspectos) añade nociones de transversalidad, que no están disponibles en la programación orientada a objetos (POO). Esta transversalidad se centra en aspectos que permiten añadir un procesamiento específico cuando se ejecuta un tipo de evento sobre un conjunto de objetos. Por lo tanto, es posible añadir aspectos como la gestión de transacciones, que es transversal a un conjunto de objetos. Spring utiliza mucho la AOP de forma transparente. Su uso en nuestros proyectos es independiente del kernel Spring. La librería Spring AOP solo se añade si es necesario.

A menudo encontramos AOP en los frameworks de nuestros clientes. Es raro que se utilice para codificar reglas de negocio porque requiere un diseño especial. Para una funcionalidad que agrupa varias entidades, generalmente es preferible copiar/pegar.

En lugar de usar un design pattern proxy, generalmente usamos el design pattern del decorador.

Decorador:

images/cap2_pag13.png

El design pattern del decorador es muy bueno, en general, porque permite especializar los objetos. Por otro lado, Java no admite la herencia múltiple y en diamante (sobre las clases).

images/cap6_pag3.png

No es posible heredar variables de más de una clase. Sin embargo, se permite la herencia de métodos de varias interfaces a través de interfaces y métodos default.

Antes de estudiar la AOP con Spring, veamos cómo modificar las clases sin la ayuda de Spring. Para hacer esto, podemos utilizar los proxys.

Si quiere ver lo que está pasando detrás de la escena, puede consultar las fuentes de los JDK (https://github.com/openjdk/jdk).

Proxy:

images/cap2_pag14.png

El design pattern proxy permite aumentar una o más clases con métodos y atributos. Escribir un proxy a mano no resulta fácil, pero es posible para una clase gracias a las API de introspección del paquete java.lang.reflect del JDK:

Método

Utilidad

Proxy

Permite crear clases e instancias de proxy dinámicas

InvocationHandler

Administrador de llamadas de los métodos en los proxys

Method

Información sobre un método

El Proxy proporciona métodos estáticos para crear clases e instancias de proxy dinámicas, y también es la superclase de todas las clases de proxy dinámicas creadas por estos métodos.

InvocationHandler es la interfaz implementada por el controlador de llamadas de una instancia de proxy. Cada instancia de proxy tiene un controlador de llamadas asociado. Cuando se llama a un método en una instancia de proxy, la llamada al método se codifica y distribuye al método invoke de su controlador de llamadas.

Un Method proporciona información y tiene acceso a un único método, clase o interfaz. El método reflejado puede ser un método de clase o de instancia (incluido un método abstracto).

Cuando creamos un proxy y lo usamos en lugar del objeto inicial correspondiente a una instanciación de clase, tenemos un método al que se llama en cada llamada de método en el proxy con el nombre del método llamado y todos sus argumentos.

Seguidamente, podemos interceptar las llamadas y llamar a los métodos reales en los objetos. También podemos añadir métodos y atributos.

Ejemplo con el JDK:

public class ProxySample { 
            public static void main(String[] args) { 
                List<String> miLista = new ArrayList<String>(); 
                miLista.add("hola"); 
                miLista.add("Proxy"); 
                miLista.add("java"); 
         
                ClassLoader loader = ProxySample.class.getClassLoader(); 
                @SuppressWarnings("unchecked") 
                List<String> proxyMiLista = (List<String>) 
        Proxy.newProxyInstance(loader, new Class<?>[] { List.class }, 
                        new MyInvocationHandler(miLista)); 
                for (int i = 0; i < 4; i++) { 
                    log(proxyMiLista.get(i)); 
                } 
            } 
         
            static class MyInvocationHandler implements InvocationHandler { 
         
                private List<String> miListaIncorporada; 
         
                public MyInvocationHandler(List<String> miLista) { 
                    this.miListaIncorporada = miLista; 
                } 
         
                @Override 
                public Object invoke(Object proxy, Method method, Object[] 
        args) throws Throwable { 
                    if (isLlamadaNo4Get(method, args)) { 
                        return "No hay cuarto elemento en la lista"; 
                    } 
                    return method.invoke(miListaIncorporada, args); 
                } 
         
                private boolean isLlamadaNo4Get(Method method, Object[] args) { 
                    return "get".equals(method.getName()) && ((Integer) 
        args[0]) == 3; 
                } 
            } 
         
            private static void log(Object msg) { 
                System.out.println(msg); 
            } 
        } 

El JDK permite hacer estos proxys en clases, pero no en interfaces. Para hacer esto, debe usar la librería CGLIB.

La API de introspección del paquete net.sf.cglib.proxy del JDK:

Método

Utilidad

Enhancer

Permite crear un proxy dinámico

MethodInterceptor

Administrador de llamadas de método en los proxys

MethodProxy

Proxy en un método

Las clases generadas por Enhancer pasan el objeto convertido en proxy a objetos MethodInterceptor registrados cuando se llama a un método interceptado. Se puede utilizar para invocar el método original o para llamar al mismo método en otro objeto del mismo tipo.

public class CGLibProxySample { 
            @SuppressWarnings("unchecked") 
            public static void main(String[] args) { 
                List<String> miLista = new ArrayList<String>(); 
                miLista.add("Hola"); 
                miLista.add("Proxy"); 
                miLista.add("java"); 
         
                log("Creación de un proxy de interfaz"); 
                List<String> proxyMiLista = (List<String>) 
        Enhancer.create(List.class, new MyInvocationHandler(miLista)); 
                for (int i = 0; i < 4; i++) { 
                    log(proxyMiLista.get(i)); 
                } 
         
                log("Creación de un proxy de clase"); 
                proxyMiLista = (List<String>) 
        Enhancer.create(ArrayList.class, new MyInvocationHandler(miLista)); 
                for (int i = 0; i < 4; i++) { 
                    log(proxyMiLista.get(i)); 
                } 
            } 
         
            static class MyInvocationHandler implements MethodInterceptor { 
         
                private List<String> miListaIncorporada; 
         
                public MyInvocationHandler(List<String> miLista) { 
                    this.miListaIncorporada = miLista; 
                } 
         
                @Override 
                public Object intercept(Object obj, Method method, Object[] 
        args, MethodProxy proxy) throws Throwable { 
                    if (isLlamadaNo4Get(method, args)) { 
                        return "No hay cuarto elemento en la lista"; 
                    } 
                    return proxy.invoke(miListaIncorporada, args); 
                } 
         
                private boolean isLlamadaNo4Get(Method method, Object[] args) { 
                    return "get".equals(method.getName()) && ((Integer) 
        args[0]) == 3; 
                } 
            } 
         
            private static void log(Object msg) { 
                System.out.println(msg); 
            } 
        } 

Con estas dos formas de proceder, podemos interceptar:

  • llamadas a métodos que pertenecen a una clase sin interfaz,

  • llamadas a métodos pertenecientes a una clase con interfaces,

  • llamadas a métodos que pertenecen a una interfaz.

Debe escribir código para:

  • enumerar métodos y sus argumentos,

  • cablear llamadas,

  • encontrar formas de declarar y añadir código para llamar antes y después de las llamadas a métodos.

Los desarrolladores del framework Spring ya han hecho todo este trabajo para las necesidades internas del framework y nos han abierto sus API.

Es más fácil ver cómo lo hicieron con la versión 0.9 de Spring.

Gracias a esto podemos orientar a aspectos nuestro código.

VM6883:64

¿Por qué AOP?

Veremos que hay una librería para usos sencillos y estándares, y otra específica para usos avanzados que se basa en anotaciones AspectJ.

Al igual que con la configuración clásica del contexto Spring, los aspectos se pueden definir como archivos XML o anotaciones, o una composición de ambos. Por ejemplo: Spring realiza la gestión de transacciones a través de la anotación @Transactional. Esta anotación es un aspecto particular gestionado directamente por Spring.

Los aspectos generalmente se colocan en la base técnica y responden a inquietudes como, por ejemplo, la instrumentación del código. Rara vez se explotan para responder a inquietudes de negocio porque este tipo de programación es compleja a nivel de mantenimiento. Por lo tanto, el código de los tratamientos de AOP debe ser limpio y estar muy bien comentado.

Los métodos llamados a través de AOP se suelen desconectar mediante configuración con anotaciones @Conditional.

Los conceptos de AOP

Spring utiliza la terminología estándar para nombrar sus elementos relativos a la AOP, incluso si no es muy intuitiva.

images/cap6_pag10.png

Definiciones

  • Aspecto: modularización de un problema que afecta a un conjunto de clases de forma transversal. Aspecto

  • Punto de unión (Joinpoint): etapa durante la ejecución de un programa, como ejecutar un método o el procesamiento de una excepción. En Spring AOP, un punto de unión siempre representa la ejecución de un método. Punto de unión Joinpoint

  • Advice: las acciones de interceptación que toma un aspecto en particular en un punto de unión. Los diferentes tipos de acciones son «alrededor de», «antes de» y «después de». Es posible encadenar estos interceptores. Añadido

  • Punto de corte (Pointcut): predicado que permite seleccionar puntos de unión mediante una expresión que indica, por ejemplo, el nombre de un método. Spring utiliza el lenguaje AspectJ de forma predeterminada. Pointcut

  • Injection: permite inyectar dinámicamente campos y métodos adicionales en un objeto. Spring AOP también permite introducir dinámicamente nuevas interfaces con sus implementaciones. Una inyección se conoce como una declaración «inter-types» en la comunidad AspectJ.

  • Objeto de destino (Target object): objeto destino de uno o más aspectos. También se llama objeto orientado a aspecto. Spring siempre usa proxys para objetos orientados a aspectos.

  • AOP proxy: un objeto creado por el marco AOP para implementar contratos de aspecto; un proxy AOP será un proxy dinámico JDK o un proxy CGLIB.

  • Weaving: vincula aspectos con otros tipos u objetos de aplicación para crear un objeto orientado a aspectos. Esto se puede hacer en tiempo de compilación, al cargar o durante la ejecución. Spring AOP mezcla aspectos durante la ejecución.

Tipos de acciones de interceptación:

  • Antes (Before): advice que se ejecuta antes de un punto de unión, pero no tiene la capacidad de impedir la ejecución del final del método, a menos que genere una excepción.

  • Después de regresar (After throwing): advice que se debe ejecutar después de un punto de unión para un método que termina sin lanzar una excepción.

  • Después de generar una excepción (After throwing): advice que se debe ejecutar si un método termina con una excepción.

  • Después (After): advice que se debe ejecutar independientemente de si se ha generado o no una excepción.

  • Advices relacionados (Around): advice que gira en torno a un punto de unión, como una llamada de método. Este es el tipo de advice más potente. El advice Around puede realizar un comportamiento personalizado antes y después de invocar al método.

Permite realizar una acción antes del método interceptado o sustituir el código del método interceptado si lo desea. También permite realizar una acción después de que se llame al método interceptado (o reemplazado) e, incluso, puede interceptar las excepciones y no transmitirlas a la persona que llama. Este último se usa a menudo porque permite hacer todo.

Se debe utilizar el tipo más sencillo de interceptación con respecto a la funcionalidad deseada. Los valores devueltos por los métodos de aspecto deben ser sencillos.

Los objetos orientados a aspecto deben conservar su independencia y, por lo tanto, no deben ver los aspectos para conservar el espíritu no invasivo de Spring.

Puede ser útil tomar medidas preventivas para evitar los aspectos en las partes del código que requieren seguridad, para evitar las interceptaciones relacionadas con la gestión de contraseñas, por ejemplo. No podemos fijarnos en un aspecto.

VM6883:64

Límites de Spring AOP y uso de AspectJ Spring AOP AspectJ

Spring AOP se limita a ejecutar métodos en puntos de unión, que es suficiente en la gran mayoría de los casos de uso. La interceptación en el cambio de estado de los campos de los objetos, por ejemplo, no se tiene en cuenta. En su lugar, AspectJ se utilizará directamente para un uso avanzado.

El soporte @AspectJ en Spring

Usamos este soporte para reutilizar la interpretación de las anotaciones de AspectJ, para la búsqueda y detección de métodos candidatos para un aspecto. Solo se utiliza el soporte de anotaciones. Spring no usa el Weaver de aspectos de AspectJ.

Es muy posible usar AspectJ junto con Spring en casos complejos donde Spring no sería suficiente.

1. Habilitar el soporte

La librería AspectJ aspectjweaver.jar debe estar en el classpath.

<dependency> 
           <groupId>aopalliance</groupId> 
           <artifactId>aopalliance</artifactId> 
           <version>1.0</version> 
        </dependency> 
        <dependency> 
           <groupId>org.aspectj</groupId> 
           <artifactId>aspectjweaver</artifactId> 
           <version>1.9.9.1</version> 
        </dependency> 

2. Habilitación de @AspectJ con configuración XML XML

Para habilitar el soporte AspectJ con una configuración basada en XML, se debe utilizar el elemento AOP aspectj-autoproxy.

Añada este elemento en la configuración y verifique que existe la definición del AOP:

<beans default-lazy-init="true" 
        xmlns="http://www.springframework.org/schema/beans" 
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context" 
        xmlns:jdbc="http://www.springframework.org/schema/jdbc"  
           xmlns:aop="http://www.springframework.org/schema/aop" 
           xsi:schemaLocation=" 
                 http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-4.1.xsd 
                 http://www.springframework.org/schema/jdbc 
        http://www.springframework.org/schema/jdbc/spring-jdbc-4.1.xsd 
                 http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-4.1.xsd 
                 http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop.xsd">  
           <aop:aspectj-autoproxy/> 
           <import resource="classpath*:spring/applicationContext.xml" /> 
           <context:component-scan base-package="fr.eni.editions" /> 
        </beans> 

3. Habilitar @AspectJ con configuración Java

Para habilitar el soporte de la gestión de los componentes marcados de la anotación @Aspect de AspectJ, hay que utilizar la anotación @Enable-AspectJAutoProxy.

Por ejemplo:

@Configuration 
         @EnableAspectJAutoProxy 
         public class AppConfig { 
         
             @Bean 
             public FooService fooService() { 
                 return new FooService(); 
             } 
         
             @Bean 
             public MyAspect myAspect() { 
                 return new MyAspect(); 
             } 
         } 

4. Declaración de un aspecto

Una vez que se ha activado @AspectJ, Spring AOP detecta automáticamente todas las clases orientadas a aspectos. Es posible declarar el aspecto para una clase mediante una anotación @Aspect (org.aspectj.lang.annotation.Aspect) o en el archivo XML.

No se aborda la declaración de aspectos en el archivo de configuración XML porque se sustituye ampliamente por anotaciones. Por defecto, el bean correspondiente a la clase orientada a aspectos es un singleton. El objeto puede contener métodos y variables, así como pointscuts, advices y declaraciones intertypes. 

La anotación @Aspect no siempre es suficiente para encontrar el bean en el classpath. Algunas veces es necesario añadir un complemento con la anotación @Component.

En general, es mejor indicar que una clase es un bean a través de una anotación Spring y, por defecto, usamos @Component.

Las clases anotadas con @Aspect pueden tener métodos y campos como el resto de las clases, pero no es posible orientar a aspectos un aspecto. Esto puede ayudar a contrarrestar los intentos de orientar a aspectos las clases críticas.

5. Declaración de un pointcut Pointcut

Spring permite interceptar un conjunto de métodos a partir de su nombre mediante la detección de puntos en común en la tipología de sus firmas.

@Pointcut("execution(* voir*(..))"public void aspectjLoadTimeWeavingExamples() { 

El pointcut se llama antes del método interceptado y puede modificar, y a continuación devolver, el objeto devuelto por el método interceptado.

AspectJ ofrece otros pointcuts que no se admiten en Spring: call, get, set, preinicialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this y @withincode. Para utilizarlos, debe cambiar a un uso completo de AspectJ.

Los siguientes verbos indican el tipo de corte:

execute

Se basa en el nombre del método.

within

Se basa en el tipo de método.

this

Se basa en la referencia de proxy del objeto.

target

Se basa en el tipo de objeto al que hace referencia el proxy.

args

Se basa en el tipo de argumento del método.

También es posible limitar los candidatos con criterios:

@target

Filtra la presencia de una anotación de ese tipo en el objeto.

@args

Filtra la presencia de una anotación de ese tipo en los argumentos del método.

@within

Filtra por la presencia de una anotación de ese tipo en el método.

@annotation

Filtra por el tipo del método destino de la anotación.

Solo los métodos públicos pueden ser interceptados. Los métodos private y protected no se tienen en cuenta para los aspectos.

Es posible combinar puntos de unión mediante el uso de operadores && (y), || (o), así como ! (negación).

@Pointcut("execution(* escuchar*(..))"public void aspectjLoadTimeWeavingExamples2() { 

Ejecución de todos los métodos públicos:

execution(public * *(..)) 

Ejecución de todos los métodos que comienzan con set:

execution(* set*(..)) 

Ejecución de todos los métodos definidos en la interfaz ClientService:

execution(* com.eni.ClientService.*(..)) 

Ejecución de todos los métodos que están en un paquete determinado:

execution(* com.eni.*.*(..)) 

Ejecución de todos los métodos que están en un paquete o subpaquete determinado:

execution(* com.eni..*.*(..)) 

Todos los puntos de acción que se encuentran en el paquete service:

within(com.eni.service.*) 

Todos los puntos de acción que se encuentran en el paquete service y sus subpaquetes:

within(com.eni.service..*) 

6. Declaración de graft sencillos Graft

Para los graft sencillos, es posible utilizar las siguientes anotaciones:

@Before

El código se ejecutará antes de la llamada al método.

@AfterReturning

El código se ejecutará después de la llamada al método.

@AfterThrowing

El código se ejecutará después de la llamada a la gestión de excepciones.

@After

El código se ejecutará después de la llamada a finally.

@Around

El código encapsula la llamada al método interceptado.

@Before se utiliza, por ejemplo, si busca la palabra «iniciar» en la firma para ejecutar un proceso antes de llamar al método.

En una turbina:

@Before("execution(* 
        fr.eni.editions.beans.Barco.iniciarTurbina(..))"public void verificarCombustibleTurbina() { 
           final Logger logger = 
        LoggerFactory.getLogger(VerificarAntes.class); 
           logger.info("antes de arrancar la turbina"); 

En todos los medios de propulsión:

@Before("execution(* iniciar*(..)) "public void verificarCombustible() { 
        System.out.println("justo antes de arrancar"); 
        } 

Por ejemplo, @After se utiliza si busca la palabra «iniciar» en la firma para ejecutar un proceso después de la llamada al método.

En todos los medios de propulsión:

@After("execution(* iniciar*(..))"public void verificarVelocidad() { 
           System.out.println("Después del arranque"); 
        } 

También es posible retornar el valor devuelto del método interceptado:

@After("execution(* iniciar*(..))"public Object verificarCombustible(Object retVal)  { 
            System.out.println("justo después de arrancar"); 
             return (retVal); 
        } 

@AfterThrowing se utiliza para tener un procesamiento después de una excepción. 

@Aspect 
        public class AfterThrowingExample { 
           @AfterThrowing("fr.eni.MiExcepcion()") 
              public void doRecoveryActions() { 
               // ... 
           } 
        } 

Es posible recuperar el tipo de la excepción para explotarlo:

@Aspect 
        public class AfterThrowingExample2 { 
           @AfterThrowing( 
              pointcut="fr.eni.MiExcepcion()",throwing="ex") 
              public void doRecoveryActions(DataAccessException ex) { 
                    // ... 
           } 
        } 

@After se utiliza para tener código después del finally.

@Aspect 
        public class AfterFinallyExample @After("fr.eni.MiExcepcion()") 
           public void doReleaseLock() { 
                    // ... 
           } 
        } 

@Around intercepta completamente una llamada de método.

El método de interceptación tiene acceso a los argumentos de la llamada del método interceptado.

@Around("execution(* iniciar*(..))"public Object generico(ProceedingJoinPoint joinPoint) throws 
        Throwable { 
           final Logger logger =  
        LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType()); 
           logger.info("Interception iniciar:"); 
           logger.info("method : " + joinPoint.getSignature().getName()); 
           logger.info("arguments : " + Arrays.toString(joinPoint.getArgs())); 
           logger.info("Around before is running!"); 
           Object ret = joinPoint.proceed(); 
           logger.info("Around after is running!"); 
           logger.info("Return " + ret.toString()); 
           return ret; 

7. Tipos genéricos Tipos:genéricos

Spring AOP admite tipos genéricos. Es posible que queramos interceptar solo ciertos tipos. En este caso, filtramos por dichos tipos.

Por ejemplo, para filtrar solo sobre MyType para la siguiente interfaz:

interface Sample<T> { 
            void sampleGenericMethod(T param); 
            void sampleGenericCollectionMethod(Collection<T> param); 

Filtramos por tipo:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && 
        args(param)"public void beforeSampleMethod(MyType param) {  
            // Advice implementation 

Para las colecciones genéricas, es más complicado. No es posible hacer este tipo de código:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) 
        && args(param)"public void beforeSampleMethod(Collection<MyType> param) { 
            // Advice implementation 

Es necesario evitarlo de la siguiente manera:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) 
        && args(param)"public void beforeSampleMethod2(Collection<?> param) { 
            // Advice implementation 

y validar para cada tipo si nos desviamos o no.

8. Determinación de nombres de argumentos

Antes de Java 8, no era posible simplemente recuperar el nombre de los argumentos de un método. Spring AOP, que sigue siendo compatible con Java 7 (y versiones superiores), utiliza estrategias para determinar los nombres de los argumentos. La forma más fácil es tener el mismo número y orden para los argumentos entre el método interceptado y el método de interceptación.

9. Orden de llamada de los grafts que interceptan el punto de unión

Los grafts se pueden llamar uno tras otro y es posible determinar un orden de paso preferente.

Es suficiente con agregar la interfaz org.springframework.core.Ordered a la clase del aspecto o añadir la anotación @Order y codificar el método getOrder():

public int getOrder() { 
         return 0; 

La clase que devuelve el orden más alto se ejecuta en primer lugar.

10. Inyección

Es posible agregar dinámicamente métodos y sus implementaciones a las clases (declaraciones intertypes con AspectJ).

Para hacer esto, creamos una clase de aspectos y añadimos la declaración del método a través de la anotación @DeclareParents. A continuación, se agrega un aspecto que indica cuándo llamar a este método.

Ejemplo:

@Component 
        @Aspect 
        public class LinkAspects {  
          @DeclareParents(value="fr.eni.spring5.TrucAdditionalDetailsImpl+", 
        defaultImpl=Truc.class) 
          public static TrucImpl trucImpl; 
        } 

11. El mecanismo de los proxys Proxy

Spring AOP utiliza proxys dinámicos del JDK para los objetos que exponen las interfaces que hay que intervenir. De lo contrario, Spring usa CGLIB.

Los métodos finales no se pueden intervenir.

Se llama al constructor del objeto convertido en proxy dos veces: una vez para el proxy y una segunda vez para el advice.

Para forzar el uso de un proxy CGLIB, debe pasar por la configuración XML:

   <aop:config proxy-target-class="true"> 
            <!-- other beans defined here... --> 
           </aop:config> 

Si utiliza la anotación @AspectJ, es suficiente con especificar en el archivo XML:

<aop:aspectj-autoproxy proxy-target-class="true"/> 

Las secciones <aop:config/> de los archivos de configuración XML se combinan en la memoria. Por lo tanto, el uso de CGLIB es común para:

  • <tx:annotation-driven/>

  • <aop:aspectj-autoproxy/>

  • <aop:config/>

Por lo tanto, la decisión de activar los CGLIB debe estar bien pensada porque puede haber efectos secundarios.

12. El lado oculto de los proxys en AOP

Es posible crear aspectos en el código, que se añaden a los declarados a través de la configuración <aop:config> o <aop:aspectj-autoproxy> complementado con anotaciones dedicadas.

Se pueden utilizar API Java simples:

package fr.eni.editions; 
        import java.lang.reflect.Method; 
        import org.springframework.aop.AfterReturningAdvice; 
        import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; 
        interface Pojo { 
            public void foo(); 
            public void bar()class SimplePojo implements Pojo { 
            public void foo() { 
                System.out.println("foo"); 
                this.bar(); 
            } 
            public void bar() { 
                System.out.println("bar"); 
            } 
        class UsageTracker implements AfterReturningAdvice { 
           public void afterReturning(Object returnValue, Method method, 
                Object[] args, Object target) throws Throwable { 
              System.out.println("after "+method.getName()); 
           } 
        public class Main2 { 
           public static void main(String[] args) { 
              AspectJProxyFactory factory = new AspectJProxyFactory(new 
        SimplePojo2()); 
              factory.addAspect(SecurityManager.class); 
        //  factory.addAspect(new UsageTracker()); 
              Pojo pojo = (Pojo) factory.getProxy(); 
           } 

Esta forma de programar resalta más fácilmente ciertos efectos secundarios relacionados con AOP y los proxys.

Normalmente, si tenemos un objeto clásico sin orientación a aspectos como este y llamamos a un método en una referencia de este objeto, llamamos directamente al código del método:

interface Pojo4 { 
            public void foo(); 
            public void bar()class MiSencilloPojo implements Pojo4 { 
            public void foo() { 
              System.out.println("foo"); 
                this.bar(); 
            } 
            public void bar() { 
              System.out.println("bar"); 
            } 
        public class Main3 { 
            public static void main(String[] args) { 
                Pojo4 pojo = new MiSencilloPojo(); 
                // this is a direct method call on the pojo reference 
                pojo.foo(); 
            } 

Se obtiene:

foo 
        bar 

Pero si añadimos algo como esto:

interface Pojo3 { 
            public void foo(); 
            public void bar()class SimplePojo3 implements Pojo3 { 
            public void foo() { 
                System.out.println("foo"); 
                this.bar(); 
            } 
            public void bar() { 
                System.out.println("bar"); 
            } 
        class RetryAdvice implements AfterReturningAdvice { 
           public void afterReturning(Object returnValue, Method method, 
                 Object[] args, Object target) throws Throwable { 
              System.out.println("after "+method.getName()); 
           } 
        public class MainConAspecto { 
            public static void main(String[] args) { 
                ProxyFactory factory = new ProxyFactory(new SimplePojo3()); 
                Class<?> intf=Pojo3.class; 
              factory.addInterface(intf); 
              Advice advice=new RetryAdvice(); 
                factory.addAdvice(advice); 
                factory.setExposeProxy(true); 
                Pojo3 pojo = (Pojo3) factory.getProxy(); 
                pojo.foo(); 
                pojo.bar(); 
            } 

Obtendremos lo siguiente:

foo 
        bar 
        after foo 
        bar 
        after bar 

Cuando estamos en una clase orientada a aspectos para la que se intercepta un método, si este método llama a un método sobre sí mismo y este método llamado es interceptado, por defecto la intercepción no tendrá lugar en el método llamado.

El código se debe refactorizar para evitar que los métodos se llamen entre sí, por ejemplo, a través de una clase de servicios intermediaria.

VM6883:64

Las API de bajo nivel para Spring AOP Spring AOP

Este es un ejemplo del uso de las API de bajo nivel. No las mostraremos todas porque se presentan muy bien en la documentación de Spring y será raro que las utilicemos. Las mencionamos porque permiten experimentar con los aspectos de manera muy sencilla.

1. La interfaz PointCut Pointcut

La interfaz PointCut se corresponde con un punto de corte que permite, a través de una expresión regular, identificar los métodos que serán interceptados.

class MiPointcut implements PointCut { 
           public ClassFilter getClassFilter() { 
              return null; 
           } 
           public MethodMatcher getMethodMatcher() { 
              return null; 
           } 

Hay dos métodos en la interfaz:

Clase

Método

Utilidad

ClassFilter

getClassFilter();

Filtrar por clase.

MethodMatcher

getMethodMatcher();

Hacer Match en la clase.

2. La interfaz ClassFilter ClassFilter

La interfaz ClassFilter solo contiene un método, que coincide con el método al que se llamará durante la interceptación.

class MiClassFilter implements ClassFilter { 
           public boolean matches(Class<?> clazz) { 
              return false; 
           } 

3. La interfaz MethodMatcher MethodMatcher

La interfaz MethodMatcher permite la evaluación estática durante la compilación, o dinámica durante la ejecución de los criterios de selección de los métodos que hay que interceptar.

class MiMethodMatcher implements MethodMatcher { 
         
          public boolean matches(Method method, Class<?> targetClass) { 
              return false; 
           } 
           public boolean isRuntime() { 
              return false; 
           } 
           public boolean matches(Method method, Class<?> targetClass, 
        Object[] args) { 
              return false; 
           } 
VM6883:64

Puntos clave

  • La programación orientada a aspectos a menudo está presente en las bases de software.

  • Es posible codificar simplemente aspectos.

  • Cuando encontramos una acción en un grupo de clases, debemos ver si podemos usar un aspecto.

  • Los aspectos son confusos; hay que entenderlos bien para poder corregir el código que los utiliza.

VM6883:64

Introducción Pruebas

La preocupación centrada en las pruebas es fundamental para los desarrollos actuales. Cada vez se utiliza más el TDD (Test-Driven Development o desarrollo basado en pruebas), que consiste en que, al mismo tiempo que se escriben nuestros programas, se escriben también las pruebas unitarias antes de escribir el código fuente del software. Desde hace algún tiempo, también se han utilizado BDD (Behavior Driven Development), que son una evolución del TDD con el que las pruebas se describen a través de frases, por ejemplo, en castellano o inglés, con una sintaxis particular, que posteriormente se procesan con un framework como Cucumber. También existen los ATDD (Acceptance Test-Driven Development), para los que los criterios de aceptación se transcriben en las pruebas.

Spring ofrece diferentes API que simplifican la implementación de pruebas unitarias (TU) y de integración (TI). Es necesario verificar y probar todo lo que vale la pena probar. Si tiene problemas al escribir los TU o TI, es porque tiene que volver a trabajar (refactorizar) su código. Una buena práctica consiste en escribir la prueba al mismo tiempo que la clase que se está probando, y algunas veces incluso codificar las pruebas antes de implementar los métodos que se están probando, para que la arquitectura de la aplicación sea compatible con las pruebas. Las pruebas unitarias son pruebas rápidas, que permiten validar acciones de bajo nivel. Las pruebas de integración utilizan juegos de pruebas para validar código de nivel superior.

También hay pruebas que simulan un usuario. Estas pruebas se realizan con herramientas como Selenium.

Spring nos ayuda proporcionando un conjunto de API especializadas para las pruebas. Encontraremos API para simular los contextos de ejecución de nuestras aplicaciones. También dispondremos de API para modificar la configuración de objetos de Spring en memoria. Para probar con datos, Spring nos ayudará dándonos acceso, de una manera muy sencilla, a conjuntos de pruebas que utilizan una base de datos en memoria. Además, tenemos acceso a un contexto Spring de prueba, que puede sobrecargar el contexto de la aplicación que se está probando.

Configuraremos las pruebas con archivos XML y anotaciones, sin perder de vista que estas pruebas deberán tener un alto grado de mantenibilidad.

De manera ideal, debemos definir desde el principio el nivel de cobertura de las pruebas TU y TI dentro de nuestro proyecto y configurarlas lo antes posible porque son muy estructurales para el código. Algunas veces estas pruebas son complejas de implementar y es necesario reservar tiempo para hacerlo. El nivel de cobertura se puede probar con JaCoCo y es habitual que se comparta en el equipo usando Sonar.

Los mock objects Mock

Cuando probamos una clase, queremos centrar nuestras pruebas en esa clase y tenemos que encontrar un sistema para no probar el resto de las clases que interactúan con la que se está probando. Podemos usar objetos simulados llamados mocks para no tener que invocar los objetos reales, lo que haría necesario un contexto de ejecución demasiado grande.

Spring ofrece un conjunto muy completo de mocks. Son más fáciles de usar que los mocks de EasyMock y MockObjects. A menudo, se utilizan con el framework Mockito (http://site.mockito.org/).

Tipo de mock

Uso

Entorno

Clases relacionadas con el entorno de ejecución.

JNDI

Simula recursos JNDI, como un origen de datos.

API de los Servlets

Simula un servlet, útil con Spring MVC.

API de los Portlets

Simula portlets Spring MVC (desaparece con Spring 5+).

Soporte

Herramientas para ayudar con la introspección de los objetos.

1. Mocks especializados por «entorno»

Se simulan clases de entorno.

Clase

Mock

Entorno

MockEnvironment

@PropertySource

MockPropertySource

Estos mocks permiten simular un entorno y un PropertySource.

2. Soporte

a. Utilidades generales

La clase ReflectionTestUtils del paquete org.springframework.test.util proporciona ayuda para la introspección y manipulación de objetos. Todos los miembros de la clase se vuelven accesibles, incluso los miembros «private».

Por ejemplo, para una clase Vehiculo:

public class Vehiculo { 
        [accesorios] 
           private long id; 
           private String modelo; 
        } 

Podemos hacer lo que queramos:

final Vehiculo persona = new Vehiculo();  
        ReflectionTestUtils.setField(persona, "id", new Long(99), 
        long.class); 
        assertEquals("id", 99L, persona.getId());  
        ReflectionTestUtils.setField(persona, "modelo", null, String.class); 
        assertNull("modelo", persona.getModelo()); 
        try {  
           ReflectionTestUtils.setField(persona, "id", null, long.class); 
           fail("Debería generar una excepción"); 
        } catch (IllegalArgumentException aExp) { 
           assert (aExp.getMessage() 
                       .contains("IllegalArgumentException")); 
        }  
        ReflectionTestUtils.invokeSetterMethod(persona, "id", new 
        Long(99), long.class); 
        assertEquals("id", 99L, persona.getId());  
          
        ReflectionTestUtils.invokeSetterMethod(persona, "setId", new 
        Long(1), long.class); 
        assertEquals("id", 1L, persona.getId()); 
        try {  
           ReflectionTestUtils.invokeSetterMethod(persona, "id", nulllong.class); 
           fail("Debería generar una excepción"); 
        } catch (IllegalArgumentException aExp) { 
           assert (aExp.getMessage() 
                        .contains("IllegalArgumentException")); 
           } 
        } 

Por lo tanto, es posible intervenir sobre variables o métodos que normalmente no están disponibles. No deberíamos necesitar estos desbloqueadores porque los métodos privados se prueban utilizando pruebas de métodos públicos.

b. Spring MVC Spring MVC

La clase ModelAndViewAssert del paquete org.springframework.test.web proporciona ayuda para probar objetos de tipo Spring MVC ModelAndView. Para probar un controlador Spring MVC, se utiliza ModelAndViewAssert en combinacióncon MockHttpServletRequest, MockHttpSession, etc. El paquete org.springframework.mock.web se basa en la API Servlet 3 desde Spring 4.0. En este capítulo vamos a ver muchos ejemplos de cómo usar estos mocks.

3. Pruebas de integración Pruebas:de integración

a. Visión general

A menudo es necesario probar los comportamientos de un conjunto de objetos para verificar, por ejemplo, la interacción entre las capas de la base de datos o las reglas de gestión que presentan conjuntos de datos. Idealmente, estas pruebas se deben realizar en un subconjunto técnico del entorno global de ejecución. Este tipo de pruebas se agrupa en las pruebas de integración. Spring permite hacer estas pruebas sin iniciar el servidor de aplicaciones o la aplicación completa. Este es uno de sus puntos fuertes.

La idea de las pruebas de integración es proporcionar los elementos para probar las diferentes capas de software, proporcionando un entorno de ejecución independiente. La librería básica para las ayudas de codificación de las pruebas se encuentra en el módulo spring-test del paquete org.springframework.test.

Estas pruebas son más lentas que las unitarias, pero más rápidas que las de Selenium (las pruebas de Selenium simulan una sesión de un usuario imitando su comportamiento en la aplicación).

Las TI se identifican utilizando anotaciones específicas para administrar, entre otras cosas, la caché del contexto para el IoC, la gestión de transacciones y la gestión de los juegos de pruebas de la base de datos.

b. Almacenamiento en caché del contexto de prueba Contexto

Para el caso de un conjunto de pruebas, la carga del contexto sería relativamente costosa si se volviera a cargar para cada prueba. Spring permite cargar un contexto y usarlo en una batería de pruebas. En caso de que una prueba altere el contexto, es posible pedirle a Spring que vuelva a cargar el contexto inicial para tener siempre un entorno limpio y estable. Incluso hay API para gestionar la degradación del contexto Spring.

Cuando se carga una batería de pruebas, especificaremos la lista de archivos de configuración que se van a cargar. En general, utilizaremos un archivo específico que complementará al archivo de configuración principal de la aplicación. A continuación, dispondremos del contexto de la aplicación principal sobrecargado para tener en cuenta todos los elementos diferentes entre el entorno de usuario y el de test.

Solo buscamos emular la base para la parte back y el servidor web para la parte front. Para programas que explotan archivos, algunas veces también utilizaremos extractos de estos archivos para probar solo los casos funcionales que pasan o no pasan.

c. Pruebas back y front

La parte back transforma los datos físicos de las bases de datos, archivos o flujos, en datos que puede utilizar una parte front que procesa o muestra los datos.

Pruebas de las partes back

Para una prueba de integración back, debe emular las fuentes de información física que provienen de bases de datos, archivos o flujos.

Bases de datos

Para las bases de datos, podemos usar clases Spring o un framework de terceros, como DbUnit o Liquibase.

Solo mostraremos los ejemplos en SQL integrados en el framework de Spring, pero le invitamos a profundizar en este asunto estudiando las posibilidades de otros frameworks.

Librerías de pruebas de Spring

Para proyectos sencillos que no usan una librería ORM, como Hibernate o JPA, y para las que el modelo de datos es pequeño, podemos usar el paquete org.springframework.test.jdbc, que contiene la clase JdbcTestUtils, la cual implementa métodos estáticos de utilidad:

countRowsInTable(..)

Cuenta el número de filas de una tabla.

countRowsInTableWhere(..)

Cuenta el número de filas de una tabla con una cláusula WHERE.

deleteFromTables(..)

Vacía una tabla.

deleteFromTableWhere(..)

Elimina filas de una tabla con una cláusula WHERE.

dropTables(..)

Elimina las tablas especificadas.

4. Anotaciones Anotaciones

a. @ContextConfiguration

La anotación principal es @ContextConfiguration. Permite cargar el contexto de pruebas de diferentes maneras. Especificamos la ubicación de los archivos de configuración, así como las clases anotadas @Configuration que llevan la configuración:

@ContextConfiguration("/test-config.xml"public class XmlApplicationContextTests { 
            // Contenido de la clase... 
        @ContextConfiguration(clases = TestConfig.class) 
        public class ConfigClassApplicationContextTests { 
            // Contenido de la clase... 

También puede especificar la clase de inicialización ApplicationContextInitializer:

@ContextConfiguration(initializers = CustomContextIntializer.class) 
        public class ContextInitializerTests // Contenido de la clase... 
        public interface ApplicationContextInitializer<C extends 
        ConfigurableApplicationContext> 

Esta interfaz se utiliza en el callback de la carga del contexto a fin de cargar las propiedades en el contexto, principalmente para tener en cuenta los argumentos context-param e init-param para las aplicaciones web.

b. @WebAppConfiguration

Esta anotación en la clase especifica que se quiere un contexto web (WebApplicationContext).

El directorio principal de la aplicación web es "file:src/main/webapp".

@ContextConfiguration 
        @WebAppConfiguration 
        public class WebAppTests { 
            // Contenido de la clase.../ 

La ruta se puede sobrecargar a través de classpath: y file:.

@ContextConfiguration  
        @WebAppConfiguration("classpath:test-web-resources") 
        public class WebAppTests { 
            // Contenido de la clase... 

c. @ContextHierarchy

Esta anotación en la clase define la jerarquía de los ApplicationContext. Se debe declarar junto con una lista de una o más anotaciones @ContextConfiguration que definan los contextos individuales que hay que considerar.

@ContextHierarchy({  
        @ContextConfiguration("/parent-config.xml"),  
        @ContextConfiguration("/hijo-config.xml") 
        }) 
        public class ContextoJerarquicoTests // Contenido de la clase... 

Se puede utilizar de manera adicional a la anotación, que define el tipo de contexto para configurar pruebas:

@WebAppConfiguration 
        @ContextHierarchy({ 
        @ContextConfiguration(clases = AppConfig.class), 
        @ContextConfiguration(clases = WebConfig.class) 
        }) 
        public class WebIntegrationTests // Contenido de la clase... 

Es posible sobrecargar una rama de contextos jerárquicos, especificando el nombre del nodo que se debe sobrecargar en el atributo name de la anotación @ContextConfiguration. Por ejemplo, el siguiente código permite aclarar la relación entre dos contextos jerárquicos para una aplicación Spring MVC:

@RunWith(SpringJUnit4ClassRunner.class) 
        @WebAppConfiguration(value = "src/main/webapp") 
        @ContextHierarchy({ 
                @ContextConfiguration(name = "parent", locations = 
                "classpath:spring-config.xml"), 
                @ContextConfiguration(name = "child", locations = 
                "classpath:spring-mvc.xml") 
        }) 

d. @ActiveProfiles

Si usamos perfiles que indican, por ejemplo, en qué entorno queremos probar, podemos usar la anotación @ActiveProfiles en una clase, que indica que esta clase solo se debe tener en cuenta si estamos en este perfil o perfiles.

@ContextConfiguration  
        @ActiveProfiles("dev") 
        public class DeveloperTests // Contenido de la clase... 
        @ContextConfiguration 
        @ActiveProfiles({"dev", "integration"}) 
        public class DeveloperIntegrationTests { 
            // Contenido de la clase... 

e. @TestPropertySource

Esta anotación en la clase define la ubicación de los archivos de recursos y las propiedades inlines que se van a añadir. Una propiedad inline es una propiedad definida sobre la marcha, directamente en la anotación.

@ContextConfiguration 
        @TestPropertySource("/test.properties"public class MyIntegrationTests // Contenido de la clase... 
        @ContextConfiguration  
        @TestPropertySource(properties = { "timezone = GMT", "port: 9999" 
        }) 
        public class MyIntegrationTests { 
            // Contenido de la clase... 

f. @DirtiesContext

Esta anotación controla la recarga del contexto cuando la prueba lo degrada. Por defecto, solo volvemos a cargar después de hacer todas las pruebas. El argumento classMode = ClassMode.AFTER_EACH_TEST_METHOD indica que queremos limpiar el contexto después de cada prueba. Si ponemos la anotación en un método de prueba, indicamos que queremos volver a cargar el contexto después de la prueba especificada.

@DirtiesContext 
        public class ContextDirtyingTests {  
        @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) 
        public class ContextDirtyingTests @DirtiesContext 
        @Test 
        public void testProcessWhichDirtiesAppCtx() { 

En caso de que la prueba esté en una jerarquía de contextos, podemos pedirle que vuelva a cargar todo el contexto.

@ContextHierarchy({ 
        @ContextConfiguration("/parent-config.xml"), 
        @ContextConfiguration("/child-config.xml") 
        }) 
        public class BaseTests // Contenido de la clase... 
        public class ExtendedTests extends BaseTests @Test  
        @DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL) 
        public void test() { [...]} 

g. Interfaz TestExecutionListener TestExecutionListener

TestExecutionListener define una API de escucha para reaccionar a los eventos de ejecución de prueba publicados con la clase TestContextManager, el punto de entrada principal en el framework Spring TestContext.

Las implementaciones concretas deben proporcionar un constructor público sin argumentos, de modo que se pueda instanciar los auditores de forma transparente.

Spring proporciona las siguientes implementaciones, preparadas para usar:

DependencyInjectionTest ExecutionListener

Proporciona soporte para inyectar dependencias e inicializar instancias de prueba.

DirtiesContextTest ExecutionListener

Proporciona soporte para marcar como modificado el ApplicationContext asociado a una prueba para las clases de prueba y los métodos de prueba anotados con la anotación @DirtiesContext.

TransactionalTest ExecutionListener

Proporciona soporte para ejecutar pruebas en transacciones administradas por pruebas que respetan la anotación @Transactional de Spring.

Esta anotación, utilizada junto con la anotación @ContextConfiguration, indica qué objetos de clase TestExecutionListener se utilizan.

@ContextConfiguration  
        @TestExecutionListeners({CustomTestExecutionListener.class, 
        AnotherTestExecutionListener.class}) 
        public class CustomTestExecutionListenerTests { 
            // Contenido de la clase... 

h. @TransactionConfiguration

Esta es una de las anotaciones más complejas. Especifica las condiciones para poder utilizar las pruebas que se realizan dentro de una transacción. La transacción permite realizar un conjunto de operaciones y validarlas, solo si todas tienen éxito. En caso de que alguna de ellas falle, todas las operaciones se cancelan. En algunos casos, se solicitará explícitamente la cancelación de los cambios al final de una secuencia de prueba. En otros casos más complejos, tendremos que completar la transacción hasta el final y restaurar el juego de pruebas.

i. @Transactional

También hay un tipo extendido de transacción: la transacción «distribuida» que involucra un conjunto de transacciones, una transacción global que envuelve un conjunto de transacciones con una validación global de todos los cambios en caso de que todas las operaciones terminen con éxito y viceversa. Este tipo de transacción se produce si, por ejemplo, tenemos varias bases de datos en juego para un solo conjunto de modificaciones. Esto se denomina confirmación multifase.

La anotación @Transactional indica que el método etiquetado participa en la transacción actual.

En caso de que haya varios TransactionManagers en la aplicación, indicamos qué TransactionManager se debe ejecutar para administrar las transacciones.

@ContextConfiguration 
        @TransactionConfiguration(transactionManager = "txMgr", 
        defaultRollback = falsepublic class CustomConfiguredTransactionalTests { 
            // Contenido de la clase... 

j. @Rollback

La anotación @Rollback indica si la transacción para el método de prueba anotado se debe anular una vez que el método de prueba termina.

Un valor en true indica que la transacción se debe anular. Se puede modificar este valor predeterminado indicando false, como en el ejemplo siguiente:

@Rollback(false) 
        @Test 
        public void testProcessWithoutRollback() // ... 

k. @BeforeTransaction

El método que tiene la anotación @BeforeTransaction se ejecuta antes de que comience la transacción. Se puede utilizar para inicializar el conjunto de pruebas.

@BeforeTransaction 
        public void beforeTransaction() { 

l. @AfterTransaction

El método con la notación @AfterTransaction se ejecuta una vez completada la transacción. Se puede utilizar para limpiar el juego de pruebas.

@AfterTransaction 
        public void afterTransaction() { 

m. @Sql, @SqlConfig y @SqlGroup

La anotación @Sql indica a Spring que debe ejecutar los scripts SQL antes de iniciar el método de pruebas. Se utiliza para anotar una clase de prueba o un método de prueba para configurar los scripts SQL que se han de ejecutar en la base de datos durante las pruebas de integración.

@Sql ({ "/test-schema.sql" , "/test-user-data.sql" }) 
        public  void usertest { 
             // ejecutar código basado en los datos del esquema 
             // y de prueba 

La anotación @SqlConfig muestra cómo parsear los archivos SQL.

@Test 
        @Sql( 
        scripts = "/test-user-data.sql", 
        config = @SqlConfig(comoPrefijo = "`", separator = "@@") 
        public void userTest { 
            // Código relativo a las pruebas de datos 

La anotación @SqlGroup permite tener varias anotaciones @Sql.

Se puede omitir desde Java 8 porque las anotaciones son «repetibles», es decir, podemos utilizar la misma anotación varias veces, pero con diferentes argumentos. Por ejemplo, podemos usar la anotación @SqlGroup en la que podemos tener un conjunto de anotaciones @Sql:

@Test  
        @SqlGroup({ 
        @Sql(scripts = "/test-schema.sql", config = 
        @SqlConfig(comoPrefijo = "`")), 
        @Sql("/test-user-data.sql") 
        )} 
        public void userTest { 
            // Código relativo a las pruebas sobre el esquema y los datos 

5. Anotaciones estándares

Las siguientes anotaciones son compatibles con la semántica estándar:

  • @Autowired

  • @Qualifier

  • @Resource (javax.annotation) si JSR-250 está presente

  • @Inject (javax.inject) si JSR-330 está presente

  • @Named (javax.inject) si JSR-330 está presente

  • @PersistenceContext (javax.persistence)

  • @PersistenceUnit (javax.persistence)

  • @Required

  • @Transactional

Advertencia: los beans para los que se configura un procesamiento de construcción y destrucción son complejos de administrar a nivel de TU y TI.

  • @PreDestroy nunca se llama.

  • @PostConstruct se llama antes que @Before.

Los frameworks de test JUnit y TestNG

Spring ha integrado los dos frameworks más completos en la actualidad para simplificar su uso. Construir un entorno para ejecutar nuestras pruebas se convierte en un juego de niños y es posible cargar en la memoria solo lo mínimo para poder hacer dichas pruebas. Hay dos versiones de JUnit: la 4 y la 5, llamada Jupiter. Es posible mezclar la sintaxis de JUnit4 con Jupiter para proyectos híbridos o en curso de migración. En adelante en este capítulo, vamos a ver las pruebas de integración que utilizan Spring y JUnit. Las pruebas que usan TestNG son similares a las que usan JUnit y no se van a detallar.

Las pruebas unitarias se realizan fuera de Spring y, por lo tanto, no se tratan aquí.

Las pruebas de integración son relativamente complejas porque queremos crear el contexto mínimo que contenga el mínimo de beans Spring que son necesarios y suficientes para probar la parte que nos interesa.

1. Utilización con JUnit 4 JUnit

La librería JUnit está muy bien documentada en este sitio: http://junit.org/. Se basa en runners que ayudan con la configuración de las pruebas.

a. Spring JUnit Runner Runner

Spring ha integrado JUnit con la clase SpringJUnit4ClassRunner.class, que se utiliza junto con la anotación @RunWith.

@RunWith(SpringJUnit4ClassRunner.class) 
        @TestExecutionListeners({}) 
        public class SimpleTest { 
            @Test 
        public void testMethod() { 
        [...] 
        } 

b. @IfProfileValue

La anotación funciona como @ActiveProfiles y se puede ubicar en la clase o en un método.

@IfProfileValue(name="java.vendor", value="Oracle Corporation") 
        @Test 
        public void testProcesoConJvmOracle() {  
         
        @IfProfileValue ( name = "test-groups" , values ={ "unit-tests""integration-tests" }) 
        @Test 
         public  void 
        testProcessWhichRunsForUnitOrIntegrationTestGroups() { 
             // una determinada lógica que debería funcionar solo 
             // para las pruebas unitarias y de integración 

c. @ProfileValueSourceConfiguration

Esta anotación en una clase especifica el tipo de ProfileValueSource. Se debe utilizar para recuperar los valores de perfil configurados por la anotación @IfProfileValue:

@ProfileValueSourceConfiguration(CustomProfileValueSource.class) 
        public class CustomProfileValueSourceTests { 

De forma predeterminada, usaremos SystemProfileValueSource.

d. @Timed

La anotación @Timed indica que el método de prueba anotado se debe completar en un plazo de tiempo especificado (en milisegundos). Si el tiempo de ejecución de la prueba excede el tiempo especificado, la prueba falla.

Solo se considera la duración de la prueba. Se puede usar junto con la anotación @Repeat, que veremos a continuación.

@Timed(millis=1000) 
        public void testProcessWithOneSecondTimeout() // Código que no debe durar más de un segundo. 

JUnit tiene una anotación similar: @Test(timeout=... ), pero ejecuta la prueba en un thread diferente y la interrumpe si se excede el timeout. Spring deja que el tratamiento vaya hasta el final e invalida la prueba si se excede.

e. @Repeat

Especifica que el método de prueba anotado se debe ejecutar de manera repetida, especificando el número de iteraciones como argumento.

@Repeat(10) 
        @Test 
        public void testProcessRepeatedly() // ... 

f. Meta-anotaciones de soporte para las pruebas Meta-anotaciones

Con Spring Framework 4 (y versiones superiores), es posible usar meta-anotaciones combinadas con anotaciones de prueba para hacer superanotaciones que agrupan múltiples anotaciones.

Lista de meta-anotaciones:

Anotación

Función

@ContextConfiguration

Define metadatos a nivel de clase que se utilizan para determinar cómo cargar y configurar un ApplicationContext para las pruebayy7,8

s de integración.

@ContextHierarchy

Anotación a nivel de clase que se utiliza para definir una jerarquía de ApplicationContext(s) para las pruebas de integración.

@ActiveProfiles

Anotación a nivel de clase que se utiliza para declarar los perfiles de definición de beans activos que se van a utilizar al cargar un ApplicationContext para las clases de prueba.

@TestPropertySource

Anotación a nivel de clase que se usa para configurar las ubicaciones de los archivos de propiedades y propiedades integradas que se van a añadir al conjunto de PropertySources del entorno para un ApplicationContext para las pruebas de integración.

@DirtiesContext

Anotación de prueba que indica que el ApplicationContext asociado a una prueba está dañado y, por lo tanto, se debe cerrar y quitar de la memoria caché de contexto.

@WebAppConfiguration

Anotación a nivel de clase utilizada para declarar que el ApplicationContext cargado para una prueba de integración debe ser un WebApplicationContext.

@TestExecutionListeners

Establece los metadatos a nivel de clase para configurar los TestExecutionListeners que se deben registrar después de un TestContextManager.

@Transactional

Describe un atributo de transacción en un método o clase individual.

@BeforeTransaction

Anotación de prueba que indica que el método anotado void se debe ejecutar antes de que se inicie una transacción por un método de prueba, configurado para ejecutarse en una transacción a través de la anotación @Transactional de Spring.

@AfterTransaction

Una anotación de prueba que indica que el método anotado void se debe ejecutar después del final de una transacción para un método de prueba, configurado para ejecutarse en una transacción a través de la anotación @Transactional de Spring.

@TransactionConfiguration

Define metadatos a nivel de clase para configurar pruebas transaccionales.

@Rollback

Anotación de prueba utilizada para indicar si una transacción administrada por prueba se debe anular cuando termine el método de prueba.

@Sql

Se utiliza para anotar una clase de prueba o un método de prueba para configurar los scripts SQL y las sentencias que se deben ejecutar en una base de datos específica durante las pruebas de integración.

@SqlConfig

Define los metadatos utilizados para determinar cómo analizar y ejecutar los scripts SQL configurados mediante la anotación @Sql.

@SqlGroup

Anotación de contenedor que agrupa varias anotaciones @Sql.

@Repeat

Anotación de prueba que se debe utilizar con JUnit 4 para indicar que se debe llamar a un método de prueba de manera repetida.

@Timed

Anotación de prueba que se debe utilizar con JUnit 4 para indicar que un método de prueba debe terminar la ejecución dentro de un período de tiempo especificado.

@IfProfileValue

Anotación de prueba que se debe utilizar con JUnit 4 para indicar si una prueba está habilitada o deshabilitada para un perfil de prueba específico.

@ProfileValueSourceConfiguration

Anotación a nivel de clase que se debe utilizar con JUnit 4 para especificar el tipo de ProfileValueSource que se va a usar durante la recuperación de los valores de perfil configurados a través de @IfProfileValue.

Las anotaciones compuestas pueden afectar a la correcta visibilidad del código. Solo se deben utilizar sabiamente y los nuevos nombres deben ser claros. En general, no es aconsejable crear una anotación con el mismo nombre que el de la anotación original, con un nombre de paquete diferente y con un comportamiento diferente.

Si descubrimos que estamos repitiendo la siguiente configuración en nuestro conjunto de pruebas JUnit:

@RunWith(SpringJUnit4ClassRunner.class)  
        @ContextConfiguration({"/app-config.xml", "/test-data-access- 
        config.xml"}) 
        @ActiveProfiles("dev") 
        @Transactional 
        public class OrderRepositoryTests { } 
        @RunWith(SpringJUnit4ClassRunner.class)  
        @ContextConfiguration({"/app-config.xml", "/test-data-access- 
        config.xml"}) 
        @ActiveProfiles("dev") 
        @Transactional 
        public class UserRepositoryTests { } 

Podemos reducir la duplicación anterior introduciendo una anotación compuesta personalizada:

@Target(ElementType.TYPE) 
        @Retention(RetentionPolicy.RUNTIME)  
        @ContextConfiguration({"/app-config.xml", "/test-data-access- 
        config.xml"}) 
        @ActiveProfiles("dev") 
        @Transactional 
        public @interface TransactionalDevTest { } 

Podemos usar nuestra anotación personalizada TransactionalDevTest para simplificar la configuración de clases de prueba individuales de la siguiente manera:

@RunWith(SpringJUnit4ClassRunner.class) 
        @TransactionalDevTest 
        public class OrderRepositoryTests { } 
        @RunWith(SpringJUnit4ClassRunner.class)  
        @TransactionalDevTest 
        public class UserRepositoryTests { } 

2. Los misterios del framework Spring TestContext TestContext

El núcleo del framework consta de dos clases y tres interfaces.

a. Clases e interfaces del framework de pruebas

Clase TestContextManager

Hay una clase TestContextManager por cada clase de prueba JUnit. Gestiona un TestContext, actualiza el estado de la prueba en el contexto, informa al TestExecutionListener de los eventos registrados como los eventos before y after, así como el IoC, la transacción, etc.

Clase TestContext

Lleva el contexto de la prueba actual y controla el almacenamiento en caché del contexto.

Interfaz TestExecutionListener

Administra eventos registrados como before y after, así como el IoC, transacción en pruebas, etc.

Interfaz ContextLoader

Carga el contexto de la aplicación para realizar la prueba. El SmartContextLoader puede cargar anotaciones. También puede cargar un contexto web.

Interfaz SmartContextLoader

Es una extensión de la interfaz ContextLoader que la sustituye desde la versión 3.1 de Spring. Permite procesar recursos localizados, clases anotadas, inicializadores de contexto, etc.

Spring proporciona un conjunto de implementaciones de estas interfaces.

Hay dos cargadores de contexto:

  • DelegatingSmartContextLoader: delega en AnnotationConfigContextLoader un GenericXmlContextLoader o un GenericGroovyXmlContextLoader, en función de la configuración declarada.

  • WebDelegatingSmartContextLoader: delega en AnnotationConfigWebContextLoader un GenericXmlWebContextLoader o un GenericGroovyXmlWebContextLoader, en función de la configuración declarada.

Un contexto web ContextLoader solo se utilizará si la anotación @WebAppConfiguration está presente en la clase de prueba.

AnnotationConfigContextLoader

Carga un ApplicationContext estándar para una clase anotada.

Por ejemplo:

@ActiveProfiles({ "foo", "bar" }) 
        @ContextConfiguration(clases = Config.class, 
        loader = AnnotationConfigContextLoader.class) 
        private static class FooBarProfilesTestCase { 
        } 

AnnotationConfigWebContextLoader

Carga un WebApplicationContext estándar para una clase anotada.

GenericXmlContextLoader

Carga un ApplicationContext a partir de un recurso XML.

Por ejemplo:

public void testLoadContext() throws Exception { 
            GenericXmlContextLoader conventionContextLoader = new 
        GenericXmlContextLoader(); 
            final ApplicationContext applicationContext = convention 
        ContextLoader.loadContext("classpath:applicationContext-test2.xml"); 

GenericXmlWebContextLoader

Carga un WebApplicationContext a partir de un recurso XML.

GenericPropertiesContextLoader

Carga un ApplicationContext a partir de un archivo de propiedades de Java.

Por ejemplo:

@ContextConfiguration(locations = "/foo.properties", 
        loader = GenericPropertiesContextLoader.class) 
        @ActiveProfiles("foo"static class PropertiesLocationsFoo { 
        } 

b. Configurar TestExecutionListener con anotaciones TestExecutionListener Anotaciones

En las siguientes secciones, se explica cómo configurar el framework TestContext con anotaciones y se proporcionan ejemplos.

Spring proporciona las siguientes implementaciones TestExecutionListener, que se registran de forma predeterminada, exactamente en este orden:

Listener

Utilidad

ServletTestExecution Listener

Configura un mock de Servlet para un WebApplicationContext.

DependencyInjection TestExecutionListener

Proporciona la inyección de dependencias para pruebas.

DirtiesContextTest ExecutionListener

Administra la anotación @DirtiesContext.

DirtiesContextBefore ModesTestExecutionListener

Proporciona soporte para inyectar dependencias e inicializar instancias de prueba.

TransactionalTest ExecutionListener

Proporciona la ejecución de las pruebas con semántica transaccional de anulación predeterminada.

SqlScriptsTest ExecutionListener

Ejecuta los scripts SQL configurados mediante anotación SQL.

c. TestExecutionListeners TestExecutionListeners

Los listeners de ejecución de pruebas TestExecutionListeners se implementan como una lista de listeners a los que se llama de manera secuencial durante una prueba. Es posible personalizar esta lista para controlar las diferentes fases de la prueba.

El registro personalizado de los TestExecutionListeners a través de la anotación @TestExecutionListeners está adaptado para los listeners personalizados, que se usan en escenarios de prueba limitados. Sin embargo, se puede hacer tedioso usar un listener personalizado en un conjunto de pruebas. Para solucionar este problema, Spring Framework 4.1 (y versiones superiores) soporta la detección automática de implementaciones TestExecutionListeners a través del mecanismo del SpringFactoriesLoader.

La lista de listeners predeterminada se encuentra en la lista org.springframework.test.context.TestExecutionListener, en el archivo de propiedades META-INF/spring.factories. Los frameworks de terceros pueden contribuir con su propio TestExecutionListener a la lista de listeners predeterminada de la misma manera, a través de su propio archivo de propiedades META-INF/spring.factories.

# Default TestExecutionListeners for the Spring TestContext Framework 
        
        org.springframework.test.context.TestExecutionListener = \ 
        org.springframework.test.context.web.ServletTestExecutionListener,\ 
        org.springframework.test.context.support.DependencyInjectionTest 
        ExecutionListener,\ 
        org.springframework.test.context.support.DirtiesContextTest 
        ExecutionListener,\ 
        org.springframework.test.context.transaction.TransactionalTest 
        ExecutionListener,\ 
        org.springframework.test.context.jdbc.SqlScriptsTestExecution 
        Listener 

Ordenar los TestExecutionListeners

Durante la detección de listeners, Spring los ordena usando la clase AnnotationAwareOrderComparator, que implementa la interfaz Ordered con el valor deseado.

Fusión de TestExecutionListeners

Si se registra un TestExecutionListener personalizado, ya no se llama a los listeners predeterminados. En este caso, debe declarar explícitamente los listeners necesarios para la prueba; por ejemplo:

@ContextConfiguration  
        @TestExecutionListeners({ 
            MyCustomTestExecutionListener.class, 
            ServletTestExecutionListener.class, 
            DependencyInjectionTestExecutionListener.class, 
            DirtiesContextTestExecutionListener.class, 
            TransactionalTestExecutionListener.class, 
        SqlScriptsTestExecutionListener.class 
        }) 
        public class MiTest { 
            // Contenido de la clase... 

En caso de cambio de versión de Spring, es necesario verificar que la consistencia de la lista de listeners por defecto siga siendo válida.

Para solucionar este problema, Spring permite combinar la lista de los listeners declarada con la lista de los listeners predeterminada, a través del atributo mergeMode de la anotación @TestExecutionListeners, que luego se debe fijar en MergeMode.MERGE_WITH_DEFAULTS.

Por ejemplo, si en la clase del ejemplo anterior con el orden del ListenerMyCustomTestExecutionListener establecido en 500, que es menor que el orden del listener ServletTestExecutionListener, con valor 1000. Entonces MyCustomTestExecutionListener se puede combinar automáticamente con la lista de valores predeterminados de ServletTestExecutionListener, y el ejemplo anterior se puede sustituir por el siguiente.

@ContextConfiguration 
        @TestExecutionListeners( 
        listeners = MyCustomTestExecutionListener.class,  
        mergeMode = MERGE_WITH_DEFAULTS, 
        public class MiTest { 
            // Contenido de la clase... 

Acceso al contexto desde la prueba

Cada contexto TestContext administra su contexto de prueba. Hay disponible una referencia del contexto si la clase de prueba implementa la interfaz ApplicationContextAware.

La clase abstracta AbstractJUnit4SpringContextTests implementa esta interfaz.

Otra forma de hacerlo es inyectar el contexto en la clase de prueba:

Para un contexto sencillo:

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration 
        public class MiTest @Autowired  
        private ApplicationContext applicationContext; 
            // Contenido de la clase... 

Para un contexto web:

@RunWith(SpringJUnit4ClassRunner.class) 
        @WebAppConfiguration 
        @ContextConfiguration 
        public class MyWebAppTest { 
            @Autowired  
        private WebApplicationContext wac; 
            // Contenido de la clase... 

La inyección la proporciona el listener DependencyInjectionTestExecutionListener, que se registra de forma predeterminada.

En las siguientes secciones se describe cómo configurar un ApplicationContext mediante archivos de configuración XML, clases anotadas (normalmente Configurationclases) o inicializadores de contexto, mediante la anotación Spring ContextConfiguration. Como alternativa, puede implementar y configurar su propio SmartContextLoader personalizado para usos avanzados.

Los archivos XML de configuración se cargan a través de una matriz de nombres de archivo. La ruta del archivo se puede basar en classpath:, file: y http:.

@RunWith(SpringJUnit4ClassRunner.class)   
        @ContextConfiguration(locations={"/app-config.xml""/test-config.xml"}) 
        public class MiTest { 
            // Contenido de la clase... 

Normalmente, apuntamos a un archivo de configuración que contiene una inclusión del archivo de configuración principal de la aplicación, además de sobrecargas de bean y configuraciones específicas de las pruebas.

El atributo "locations=" es opcional:

@RunWith(SpringJUnit4ClassRunner.class)  
        @ContextConfiguration({"/app-config.xml", "/test-config.xml"}) 
        public class MiTest { 
            // Contenido de la clase... 

Si no especificamos los atributos locations y value, Spring intenta inferir la ubicación del archivo de configuración de las pruebas. Los cargadores GenericXmlContextLoader y GenericXmlWebContextLoader se basan en el nombre de la prueba con el paquete y buscan en el classpath un archivo de configuración correspondiente al nombre de la clase con el sufijo -context.xml.

Por ejemplo, para com.example.MiTest, el cargador GenericXmlContextLoader carga el contexto de la aplicación desde "classpath:com/example/MiTest-context.xml".

package com.example; 
        @RunWith(SpringJUnit4ClassRunner.class) 
        // ApplicationContext se cargará desde:  
        // "classpath:com/example/MiTest-context.xml" 
        @ContextConfiguration 
        public class MiTest{ [...] 

Mediante los cargadores AnnotationConfigContextLoader y AnnotationConfigWebContextLoader, Spring detecta las clases de configuración en las clases internas estáticas incluidas en la clase de prueba:

@RunWith(SpringJUnit4ClassRunner.class) 
        // ApplicationContext se carga desde la clase interna 
        // 
        @ContextConfiguration 
        public class OrderServiceTest { 
            @Configuration 
        static class Config { 
                @Bean 
        public OrderService orderService() { 
                    OrderService orderService = new OrderServiceImpl(); 
        [...] 
        return orderService; 
                } 
            } 
        private OrderService orderService; 
            @Test 
        public void testOrderService() { 
        [...] 
        } 

d. Mezcla XML y clases anotadas XML

Es posible apuntar a un recurso de prueba (archivo o clase anotada) que incluya dentro de sí una configuración mixta. Para las preocupaciones relacionadas con el mantenimiento, se debe tener cuidado de ser coherente y constante en las reglas de escritura de código en la aplicación.

3. Configuración de los contextos de prueba

Los contextos de prueba se pueden configurar mediante anotaciones y archivos. 

a. Configuración de contexto con inicializadores de contexto Contexto:configuración

Para usar inicializadores de contexto para la prueba, debe anotar la clase con la anotación @ContextConfiguration y proporcionar al atributo initializers una matriz de clases que implementen la interfaz ApplicationContextInitializer. A continuación, se utilizan para inicializar el ConfigurableApplicationContext cargado para las pruebas.

El orden en que se invocan los inicializadores se puede establecer mediante la interfaz Ordered o las anotaciones @Order o @Priority.

@RunWith(SpringJUnit4ClassRunner.class)  
        @ContextConfiguration( 
        clases = TestConfig.class,  
        initializers = TestAppCtxInitialiseur.class) 
        public class MiTest // Contenido de la clase... 

La ausencia de declaraciones de los archivos de configuración o clase de configuración anotada no impide la declaración de inicializadores:

@RunWith(SpringJUnit4ClassRunner.class) 
        // ApplicationContext será inicializada por el EntireAppInitializer 
        @ContextConfiguration(initializers = EntireAppInitializer.class) 
        public class MiTest // Contenido de la clase...  

b. Herencia en la configuración de los contextos Herencia

Podemos deshabilitar la herencia de configuración estableciendo el valor false para los atributos inheritLocations y inheritInitializers de la anotación @ContextConfiguration. El efecto de esto es que, en caso de heredar de la clase de prueba, la clase heredada sobrescribe la configuración de la clase que hereda, en lugar de completarla en términos de ubicación de los archivos y lista de inicializadores.

La configuración de la clase ExtendedTest se compone inicialmente de los valores de configuración de la clase BaseTest, que se complementan con los valores de configuración de la clase ExtendedTest.

Si uno de los valores está duplicado, el valor de la configuración hija gana.

Esto funciona de la misma manera para las configuraciones basadas en clases anotadas:

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration(clases = BaseConfig.class) 
        public class BaseTest // Contenido de la clase... 
        @ContextConfiguration(clases = ExtendedConfig.class) 
        public class ExtendedTest extends BaseTest // Contenido de la clase... 

El mismo principio se aplica a los inicializadores, excepto que el orden se tiene en cuenta:

@RunWith(SpringJUnit4ClassRunner.class) 
        // ApplicationContext inicializado por BaseInitializer  
        @ContextConfiguration(initializers = BaseInitializer.class) 
        public class BaseTest // Contenido de la clase... 
        // ApplicationContext inicializado por BaseInitializer 
        // et ExtendedInitializer  
        @ContextConfiguration(initializers = ExtendedInitializer.class) 
        public class ExtendedTest extends BaseTest // Contenido de la clase...  

c. Soporte de los perfiles de entorno Perfiles de entorno

Desde Spring 3.1, es posible especificar perfiles de entorno. El perfil se utiliza como criterio para aplicar o no un elemento de la configuración del contexto de Spring. La anotación @ActiveProfiles se utiliza para enumerar los perfiles para los que se debe lanzar la prueba. Es necesario el cargador SmartContextLoader para tener en cuenta las clases anotadas. El cargador ContextLoader no es suficiente.

Archivo App-config.xml:

<!-- app-config.xml --> 
        <beans xmlns="http://www.springframework.org/schema/beans" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
            xmlns:jdbc="http://www.springframework.org/schema/jdbc" 
            xmlns:jee="http://www.springframework.org/schema/jee" 
        xsi:schemaLocation="..."> 
        <bean id="transferService" 
        class="com.banco.service.internal.DefaultTransferService"> 
        <constructor-arg ref="CuentaDAO"/> 
        <constructor-arg ref="politicaTarifaria"/> 
        </bean> 
        <bean id="CuentaDAO" 
        class="fr.eni.DAO.internal.JdbcAccountRepository"> 
        <constructor-arg ref="dataSource"/> 
        </bean> 
        <bean id="politicaTarifaria" 
                class="fr.eni.service.internal.SinImpuestos"/>  
        <beans profile="dev"> 
        <jdbc:embedded-database id="dataSource"> 
        <jdbc:scriptlocation="classpath:com/banco/config/sql/schema.sql"/> 
        <jdbc:scriptlocation="classpath:com/banco/config/sql/test-data.sql"/> 
        </jdbc:embedded-database> 
        </beans>  
        <beans profile="production"> 
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> 
        </beans>  
        <beans profile="default"> 
        <jdbc:embedded-database id="dataSource"> 
        <jdbc:scriptlocation="classpath:com/banco/config/sql/schema.sql"/> 
        </jdbc:embedded-database> 
        </beans> 
        </beans> 

Código Java:

packagefr.eni.banco.service; 
        @RunWith(SpringJUnit4ClassRunner.class) 
        // ApplicationContext cargado desde "classpath:/app-config.xml" 
        @ContextConfiguration("/app-config.xml")  
        @ActiveProfiles("dev") 
        public class TransferServiceTest { 
            @Autowired 
        private TransferService transferService; 
        @Test 
        public void testTransferService() { 
                // prueba del transferService 
        } 

La fuente de datos se configura de forma diferente en función del entorno:

  • Producción: base de datos a través de JNDI.

  • Dev: base de datos incrustada con inicialización de los valores.

  • Default: base de datos incorporada sin inicialización de valores.

Algunas pruebas solo se realizan en entornos específicos. En el ejemplo anterior, especificar @ActiveProfiles("dev") indica que las pruebas presentes en la clase TransferServiceTest solo se realizan en el entorno de desarrollo.

Como se ha explicado anteriormente, la configuración puede ser diferente en función de los entornos. Por ejemplo, la fuente de datos (datasource) se puede personalizar en función del entorno en el que se ejecutan las pruebas. La base de datos a la que se dirigen las pruebas es específica para cada entorno.

Existe la misma lógica con las clases anotadas. Si convertimos la configuración anterior en forma anotada usando cuatro clases para la configuración (para HSQL), tendremos:

@Configuration 
        @Profile("dev") 
        public class DatosSoloConfig @Bean 
        public DataSource dataSource() return new EmbeddedDatabaseBuilder() 
                  .setType(EmbeddedDatabaseType.HSQL) 
                  .addScript("classpath:com/banco/config/sql/schema.sql") 
                  .addScript("classpath:com/banco/config/sql/test-data.sql") 
                  .build(); 
            }  
        @Configuration 
        @Profile("production") 
        public class DatosJndiConfig { 
            @Bean 
        public DataSource dataSource() throws Exception { 
                Context ctx = new InitialContext(); 
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); 
            }  
        @Configuration 
        @Profile("default") 
        public class DatosEstandaresConfig { 
            @Bean 
        public DataSource dataSource() return new EmbeddedDatabaseBuilder() 
                    .setType(EmbeddedDatabaseType.HSQL) 
                    .addScript("classpath:com/banco/config/sql/schema.sql") 
                    .build(); 
            } 
        @Configuration 
        public class TransferServiceConfig { 
            @Autowired DataSource dataSource; 
            @Bean 
        public TransferService transferService() return new DefaultTransferService(accountRepository(), 
        feePolicy()); 
            } 
            @Bean 
        public AccountRepository accountRepository() return new JdbcAccountRepository(dataSource); 
        } 
            @Bean 
        publicPoliticaTarifaria() { 
                return new SansImpuestos(); 
        } 
        package com.banco.service; 
        @RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration(clases = { 
                TransferServiceConfig.class, 
        DatosStandardsConfig.class, 
        DatosJndiConfig.class, 
        DefaultDataConfig.class}) 
        @ActiveProfiles("dev") 
        public class TransferServiceTest { 
            @Autowired 
        private TransferService transferService; 
            @Test 
        public void testTransferService() // test unitario del servicio 
        } 

Obtenemos las siguientes variantes:

TransferServiceConfig

Adquiere un dataSource mediante la inyección de dependencias con ayuda de Autowired.

DataStandardsConfig

Define un dataSource para una base de datos integrada, adecuada para las pruebas de desarrolladores.

DataJndiConfig

Define un dataSource que se recupera a partir de JNDI en un entorno de producción.

DataStandardsConfig

Define un dataSource para una base de datos predeterminada, integrada en caso de que no haya ningún perfil activo.

Para simplificar el código, generalmente usaremos el principio de herencia:

package com.banco.service; 
        @RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration(clases = { 
        TransferServiceConfig.class, 
        DonneesStandardsConfig.class, 
        DonneesJndiConfig.class, 
        DonneesStandardsConfig.class})  
        @ActiveProfiles("dev") 
        public abstract class AbstractIntegrationTest package com.banco.service;  
        // "dev" perfil heredado de la superclase 
        public class TransferServiceTest extends AbstractIntegrationTest { 
            @Autowired 
        private TransferService transferService; 
            @Test 
        public void testTransferService() { 
                // Test unitario del transferService 
            } 
        package com.banco.service;  
        // "dev" perfil sobrecargado por "production"  
        @ActiveProfiles(profiles = "production", inheritProfiles = false) 
        public class ProductionTransferServiceTest extends 
        AbstractIntegrationTest { 
            // el test 

También podemos usar configuraciones externas para habilitar o deshabilitar ciertas pruebas. Es fácil seleccionar qué pruebas activar en función de argumentos externos como:

  • el sistema operativo,

  • la ubicación de las pruebas; por ejemplo, si desea detectar que la prueba se está ejecutando en un servidor de build de integración continua,

  • la presencia de ciertas variables de entorno,

  • la presencia de anotaciones personalizadas de nivel de clase,

  • etc.

Usaremos ActiveProfilesResolvers personalizados y registraremos los selectores de perfiles a través del atributo resolver de la anotación @ActiveProfiles

Por ejemplo, podemos implementar un selector personalizado OperatingSystemActiveProfilesResolver para filtrar en el sistema operativo:

package com.banco.service; 
        // "dev" perfil sobrecargado por un custom resolver 
        @ActiveProfiles( 
        resolver = OperatingSystemActiveProfilesResolver.class, 
        inheritProfiles = falsepublic class TransferServiceTest extends AbstractIntegrationTest 
        { 
            // test body 
        package com.banco.service.test; 
        public class OperatingSystemActiveProfilesResolver implements 
        ActiveProfilesResolver { 
            @Override 
        String[] resolve(Class<?> testClass) { 
                String profile = ...; 
        return new String[] {profile}; 
        } 

d. Configuración de contexto con archivos de propiedades de prueba Archivo de propiedades

Desde Spring 3.1 existe la opción de utilizar una jerarquía de .properties. Desde Spring 4.1 tenemos lo mismo, pero en este caso para las pruebas.

A diferencia de la anotación @PropertySource, que se usa en clases anotadas con @Configuration, la anotación @TestPropertySource se puede declarar en una clase de prueba para declarar las ubicaciones de recursos para archivos de propiedades de prueba o propiedades «inline». Estos archivos de propiedades de prueba se agregarán al set del entorno para todo el conjunto de las PropertySources para el contexto ApplicationContext, cargados para la prueba de integración anotada.

La anotación @TestPropertySource solo se puede utilizar a través de SmartContextLoader.

e. Declarar un archivo de propiedades para las pruebas

Los archivos de propiedades de prueba se pueden configurar mediante los atributos location de la anotación @TestPropertySource.

La anotación soporta los archivos .properties y .xml con una ruta relativa al classpath o al sistema de archivos:

  • "classpath:/com/example/test.properties"

  • "file:///path/to/file.xml"

Cada recurso se interpreta como un archivo de recursos de Spring.

Un path relativo se interpretará como una ruta en el paquete de clases, mientras que un archivo que comience con un «/» se tomará como en el directorio absoluto correspondiente. Una ruta en formato URL se interpretará en función del prefijo classpath:, file:, http:.

Los comodines no están permitidos en el nombre del archivo.

@ContextConfiguration 
        @TestPropertySource ("/test.properties"public class MyIntegrationTests // cuerpo de la clase... 

Las propiedades inline se declaran como pares clave-valor y se pueden configurar mediante el atributo properties de la anotación @TestPropertySource.

Todas las parejas clave-valor se agregarán al entorno que lo engloban. La sintaxis de soporte para las parejas clave-valor es la misma que la definida para las entradas en un archivo de propiedades Java:

  • "Clave = valor"

  • "Clave: valor"

  • "Valor de la clave"

@ContextConfiguration 
        TestPropertySource ( properties= {"huso horario GMT =", "puerto: 4242"}) 
        public class MisPruebasDeIntegracionTests { 
             // cuerpo de la clase... 

f. Detección del archivo de propiedades predeterminado

Si la anotación @TestPropertySource está vacía (sin valor para los atributos locations o properties), se buscará un archivo de propiedades con el nombre de la clase en el directorio correspondiente al paquete de la clase.

Si no se encuentra, Spring lanza una excepción IllegalStateException.

Prioridades

  • Propiedades en línea. Propiedades inline

  • Archivos de propiedades de prueba.

  • Propiedades del entorno del sistema operativo.

  • Propiedades del sistema Java.

  • Propiedades a través de la anotación @PropertySource.

  • Propiedades por programación.

4. Jerarquía de los contextos de prueba

Es posible tener una jerarquía de configuración de contexto para las pruebas.

a. Herencia y sobrecarga de propiedades de prueba Herencia Sobrecarga

La anotación @TestPropertySource tiene los atributos inheritLocations e inheritProperties. inheritProperties

  • inheritLocations indica de qué clase se hereda. inheritLocations

  • inheritProperties indica el modo de herencia. Si es true, los valores se añaden o sobrescriben los valores de la clase de la que se hereda. Si es false, las propiedades ocultan y reemplazan las propiedades de la clase original.

En el ejemplo siguiente, ApplicationContext para BaseTest se cargará utilizando solo la propiedad inline key1. En su lugar, ApplicationContext para ExtendedTest se cargará con las propiedades inline Key1 y Key2.

TestPropertySource (Propiedades = "key1 = value1") 
        ContextConfiguration 
        public class BaseTest { 
             // ... 
        TestPropertySource (Propiedades = "key2 = value2") 
        ContextConfiguration 
        public class ExtendedTest extends BaseTest { 
             // ... 

b. Cargar un WebApplicationContext WebApplicationContext

Spring 3.2 introdujo soporte para la anotación @WebApplicationContext, que proporciona un contexto de prueba específico para las pruebas web. Simplemente, anote una clase de prueba con la anotación @WebApplicationContext para beneficiarse del framework de prueba TestContext.

El framework proporciona un mock MockServletContext que apuntará al directorio web predeterminado "src/main/webapp". La ruta predeterminada se puede cambiar agregando un argumento a la anotación @WebAppConfiguration

WebApplicationContext funciona como ApplicationContext. Por lo tanto, es posible configurarlo a través de un archivo de configuración XML, una anotación @Configuration a través de la anotación @ContextConfiguration. También es posible utilizar otras anotaciones, como @TestExecutionListeners, @TransactionConfiguration, @ActiveProfiles, etc.

c. Convenciones

Por convención, Spring busca el archivo asociado con la prueba a partir del nombre y el paquete en el que se encuentra la prueba.

El siguiente ejemplo muestra como Spring busca el archivo WacTests-context.xml en el directorio del paquete asociado a la clase.

@RunWith(SpringJUnit4ClassRunner.class) 
        @WebAppConfiguration 
        @ContextConfiguration 
        public class WacTests //... 

En el siguiente ejemplo, se declara explícitamente una ruta de acceso al archivo de recursos con la anotación @WebAppConfiguration y una ubicación de recursos XML con @ContextConfiguration. Aquí, lo importante es tener en cuenta la diferente semántica para las rutas con estas dos anotaciones. De forma predeterminada, las rutas de recursos @WebAppConfiguration se basan en un sistema de archivos, mientras que la ubicación de recursos se basa en el classpath con la anotación @ContextConfiguration.

@RunWith(SpringJUnit4ClassRunner.class) 
        // Recursos del sistema de archivos  
        @WebAppConfiguration("webapp") 
        // classpath resource 
        @ContextConfiguration("/spring/test-servlet-config.xml"public class WacTests { 
            //... 

d. La semántica de los recursos explícitos

En este tercer ejemplo, vemos que podemos sustituir la semántica de recursos predeterminada para ambas anotaciones especificando un prefijo de recurso de Spring.

@RunWith(SpringJUnit4ClassRunner.class) 
        // classpath resource  
        @WebAppConfiguration("classpath:test-web-resources") 
        // Recurso del sistema de archivos 
        @ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml"public class WacTests { 
            //... 

Para tener un soporte completo de pruebas web, desde la versión 3.2 Spring introduce un listener ServletTestExecutionListener, que está habilitado por defecto.

Cuando se usa un contexto WebApplicationContext, este TestExecutionListener configura el estado local del subthread a través del RequestContextHolder de Spring Web, antes de cada método de prueba, y crea MockHttpServletRequest, MockHttpServletResponse y ServletWebRequest en función de la ruta de acceso del recurso base configurado mediante WebAppConfiguration. ServletTestExecutionListener también garantiza que MockHttpServletResponse y ServletWebRequest se puedan inyectar en la instancia de prueba y, una vez completada la prueba, limpia el estado del thread local.

Una vez que haya cargado un WebApplicationContext para su prueba, es posible que descubra que necesita interactuar con los mocks Webs, por ejemplo, para configurar su conjunto de pruebas o para realizar aserciones después de llamar a su componente web. En el siguiente ejemplo se muestra qué mocks pueden estar autowired en la instancia de prueba. Tenga en cuenta que tanto WebApplicationContext como MockServletContext se almacenan en caché en el conjunto de pruebas, mientras que el resto de los mocks que son controlados por los métodos de prueba se almacenan en caché mediante ServletTestExecutionListener.

e. Inyectar objetos mokeados

Podemos agregar objetos mokeados presentes en el paquete org.springframework.mock.web, como en el siguiente ejemplo:

@WebAppConfiguration 
        @ContextConfiguration 
        public class WacTests { 
            @Autowired 
            WebApplicationContext wac; // cached 
            @Autowired 
            MockServletContext servletContext; // cached 
            @Autowired 
            MockHttpSession session; 
            @Autowired 
            MockHttpServletRequest request; 
            @Autowired 
            MockHttpServletResponse response; 
            @Autowired 
            ServletWebRequest webRequest; 
            //... 

f. Cacheado del contexto de prueba

El framework de pruebas TestContext carga el contexto solo una vez para la misma configuración de prueba. Para encontrar su camino, Spring genera una clave de la configuración de prueba con los siguientes elementos:

  • locations (de @ContextConfiguration),

  • clases (de @ContextConfiguration),

  • contextInitializerClasses (de @ContextConfiguration),

  • contextLoader (de @ContextConfiguration),

  • parent (de @ContextHierarchy),

  • activeProfiles (de @ActiveProfiles),

  • propertySourceLocations (de @TestPropertySource),

  • propertySourceProperties (de @TestPropertySource),

  • resourceBasePath (de @WebAppConfiguration).

A continuación, carga el contexto en un contexto estático y lo almacena en una caché estática con esa clave.

Por ejemplo, si la clase TestClassA especifica {"app-config.xml", "test-config.xml"} para los atributos locations (o value) y las ubicaciones de @ContextConfiguration, el framework TestContext carga el contexto correspondiente y lo almacena en una caché estática, con una clave que solo se basa en esas ubicaciones. Por lo tanto, si la clase TestClassB también define {"app-config.xml", "test-config.xml"} para sus ubicaciones (explícita o implícitamente por herencia), pero no define @WebAppConfiguration, ni ningún otro ContextLoader o diferentes perfiles activos o diferentes inicializadores de contexto o diferentes archivos de propiedades de prueba o un contexto padre diferente, entonces ambas clases de prueba compartirán el mismo ApplicationContext.

Esto significa que solo se incurre una vez en el coste de instalación para cargar un contexto de aplicación (como resultado de las pruebas) y la siguiente prueba es mucho más rápida de ejecutar.

Para comprobar la coherencia con respecto al número de contextos creados, es posible mostrar las estadísticas de caché estableciendo el nivel de log de org.springframework.test.context.cache en DEBUG.

Al probar alteraciones voluntarias del contexto, es posible marcar el método de prueba con la anotación @DirtiesContext para forzar la recarga del contexto después de que se haya ejecutado el método. También es posible anotar la clase, lo que provoca la recarga del contexto después de cada llamada al método, pero esta posibilidad se debe usar con moderación porque cargar el contexto lleva tiempo.

El soporte de esta anotación lo proporciona el listener DirtiesContextTestExecutionListener, que está habilitado de forma predeterminada.

g. Jerarquías de contexto Contexto:jerarquía

Para pruebas simples, usaremos un entorno de contexto único o monocontexto. Sin embargo, en cuanto empecemos a hacer pruebas algo complejas, tendremos una jerarquía de contextos con el fin de evitar elementos redundantes en diferentes pruebas. La buena idea es reutilizar tantas cosas como sea posible entre las pruebas.

Por ejemplo, si está desarrollando una aplicación web Spring MVC, normalmente tendrá un contexto raíz WebApplicationContext, cargado a través de ContextLoaderListener, y un contexto hijo WebApplicationContext, cargado a través de DispatcherServlet. El resultado es una jerarquía de contexto padre-hijo donde los componentes compartidos y la configuración de la infraestructura se declaran en el contexto raíz y se consumen en el contexto hijo por componentes específicos en la web. Otro caso de uso se presenta en las aplicaciones de procesamiento por lotes (batch), en las que a menudo se tiene un contexto padre que proporciona la configuración de la infraestructura por lotes compartida y un contexto hijo para configurar un trabajo de un lote específico.

Desde Spring Framework 3.2.2, es posible escribir pruebas de integración que utilicen jerarquías de contexto declarando una configuración de contexto mediante anotación @ContextHierarchy. Si se declara una jerarquía de contexto en varias clases de una jerarquía de clases de prueba, podemos combinar o sustituir la configuración de contexto para un nivel con nombre específico en la jerarquía de contexto. Para que funcione la combinación de configuraciones para un nivel determinado en la jerarquía de tipos de recursos de configuración operativos, los archivos de configuración XML o las clases anotadas deberán ser coherentes. Tendremos diferentes niveles en una jerarquía de frameworks y se configurará utilizando diferentes tipos de recursos.

Los siguientes ejemplos, basados en JUnit, muestran escenarios de configuración comunes para pruebas de integración que requieren el uso de jerarquías de contexto.

ControllerIntegrationTests representa un escenario típico de pruebas de integración para una aplicación web Spring MVC. Declara una jerarquía de framework que consta de dos niveles, uno para la raíz WebApplicationContext (cargada mediante la clase TestAppConfig con la anotación @Configuration) y el otro para el servlet de distribución WebApplicationContext (cargado mediante la clase WebConfig con la anotación @Configuration). El WebApplicationContext que se inyecta en la instancia de prueba es el del contexto hijo (es decir, el contexto más bajo en la jerarquía).

@RunWith(SpringJUnit4ClassRunner.class) 
        @WebAppConfiguration  
        @ContextHierarchy({ 
        @ContextConfiguration(clases = TestAppConfig.class), 
        @ContextConfiguration(clases = WebConfig.class) 
        }) 
        public class ControllerIntegrationTests { 
            @Autowired 
        private WebApplicationContext wac; 
        // ... 

Las siguientes clases de prueba definen una jerarquía de contextos dentro de una jerarquía de clases de prueba. La clase AbstractWebTests declara la configuración para una raíz WebApplicationContext en una aplicación web de Spring. Sin embargo, tenga en cuenta que AbstractWebTests no declara @ContextHierarchy. Las subclases de AbstractWebTests pueden participar opcionalmente en una jerarquía de contextos simplemente siguiendo la semántica estándar para @ContextConfiguration. Las clases SoapWebServiceTests y RestWebServiceTests permiten al mismo tiempo extender AbstractWebTests y definir una jerarquía contextual a través de @ContextHierarchy. Como resultado, se cargarán los tres contextos de aplicación (uno para cada declaración @ContextConfiguration). El contexto de aplicación cargado en función de la configuración en AbstractWebTests lo establece el contexto padre para cada uno de los contextos cargados para las subclases concretas.

@RunWith(SpringJUnit4ClassRunner.class) 
        @WebAppConfiguration 
        @ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml"public  abstract  class AbstractWebTests {}  
        @ContextHierarchy( @ContextConfiguration("/spring/soap-ws-config.xml"public  class SoapWebServiceTests extends AbstractWebTests {}  
        @ContextHierarchy( @ContextConfiguration("/spring/rest-ws-config.xml"public  class RestWebServiceTests extends AbstractWebTests {} 

Las siguientes clases muestran el uso de niveles jerárquicos con nombre para combinar la configuración de niveles específicos en una jerarquía de contexto. BaseTests define dos niveles de la jerarquía, padre e hijo. ExtendedTests extiende BaseTests y carga el framework TestContext para combinar la configuración de contexto para el nivel de jerarquía hijo, simplemente haciendo que los nombres declarados a través del atributo name de ContextConfiguration, sean todos «hijo».

El resultado es que se cargan varios contextos de aplicación:

  • uno para "/app-config.xml",

  • uno para "/user-config.xml",

además del de "/order-config.xml".

Al igual que en el ejemplo anterior, el contexto de la aplicación cargado de "/app-config.xml" se establecerá como el contexto padre para los contextos cargados de "/user-config.xml" y {"/user-config.xml", "/order-config.xml"}.

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextHierarchy({  
        @ContextConfiguration(name = "parent", locations = "/app-config.xml"), 
        @ContextConfiguration(name = "child", locations = "/user-config.xml") 
        }) 
        public class BaseTests {} 
        @ContextHierarchy(  
        @ContextConfiguration(name = "child", locations = "/order-config.xml") 

A diferencia del ejemplo anterior, este ejemplo muestra cómo sustituir la configuración por un nivel con nombre determinado en una jerarquía de contexto estableciendo los flags inheritLocations de ContextConfiguration en false. Por lo tanto, el contexto de la aplicación para ExtendedTests solo se cargará desde "/test-user-config.xml" y tendrá su padre ajustado en el framework cargado de "/app-config.xml ".

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextHierarchy({  
        @ContextConfiguration(name = "padre", locations = "/app-config.xml"), 
        @ContextConfiguration(name = "hijo", locations = "/user-config.xml") 
        }) 
        public class BaseTests {} 
        @ContextHierarchy@ContextConfiguration( 
        name = "hijo", 
        locations = "/test-user-config.xml",  
        inheritLocations = false 
        )) 
        public class ExtendedTests extends BaseTests {} 

h. Inyección de dependencias en las pruebas Inyección de dependencias

Cuando se utiliza DependencyInjectionTestExecutionListener, que está configurado de forma predeterminada, las dependencias de las instancias de prueba se inyectan a partir de beans en el contexto de la aplicación que ha configurado con ContextConfiguration.

El framework TestContext no se preocupa por cómo se crea una instancia de prueba. Por lo tanto, el uso de @Autowired o @Inject para los constructores no tiene ningún efecto para las clases de prueba. Los campos inyectados por la anotación @Autowired se identifican por su tipo. Para diferenciarlos, se debe utilizar la anotación @Qualifier.

A partir de Spring 3, es posible utilizar la anotación @Inject junto con la anotación @Named. Como alternativa, si su clase de prueba tiene acceso a su ApplicationContext, puede realizar una búsqueda explícita utilizando (por ejemplo) una llamada a applicationContext.getBean ("miDAO")

Si no desea que se aplique la inyección de dependencias a sus instancias de prueba, es suficiente con no anotar los campos o métodos setter con @Autowired o @Inject. Como alternativa, puede deshabilitar la inserción de dependencias mientras configura explícitamente su clase con @TestExecutionListeners y omitiendo DependencyInjectionTestExecutionListener.class de la lista de listeners. Esto se debe a que es DependencyInjectionTestExecutionListener el que extiende el TestExecutionListener, que admite la inyección de dependencias y la inicialización de instancias de prueba.

Ejemplo

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration("dao-config.xml"public class HibernateTituloDAOTests { 
            @Autowired 
        private HibernateTituloDAO tituloDAO; 
            @Test 
        public void findById() { 
                Titulotitulo = tituloDAO.findById(new Long(10)); 
        assertNotNull(titulo); 
        } 

Como alternativa, puede utilizar @Autowired para configurar la clase para la inyección:

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration("dao-config.xml"public class HibernateTituloDAOTests private HibernateTituloDAO tituloDAO;  
            @Autowired 
        public void setTituloDAO(HibernateTituloDAO tituloDAO) this.tituloDAO = tituloDAO; 
            } 
            @Test 
        public void findById() { 
        Titulo titulo = tituloDAO.findById(new Long(10)); 
        assertNotNull(title); 
        } 

Las listas de código anteriores utilizan el mismo archivo de contexto XML (dao-config.xml) al que hace referencia la anotación @ContextConfiguration:

<?xml version="1.0" encoding="UTF-8"?> 
        <beans xmlns="http://www.springframework.org/schema/beans" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
            xsi:schemaLocation="http://www.springframework.org/schema/beans 
                http://www.springframework.org/schema/beans/spring-beans.xsd"> 
        <!-- this bean will be injected into the HibernateTitulo 
        HibernateTituloDAO DaoTests class --> 
        <bean id="tituloDAO " class="fr.eni.dao.hibernate. 
        HibernateTituloDAO "> 
        <property name="sessionFactory" ref="sessionFactory"/> 
        </bean> 
        <bean id="sessionFactory" 
        class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> 
        <!-- configuration elided for brevity --> 
        </bean> 
        </beans> 

5. El scope session durante una prueba de consulta

Es posible usar los scopes para beans durante las pruebas.

a. El scope session durante una prueba de consulta Scope session

Los beans de scope session han estado disponibles en Spring durante mucho tiempo, pero siguen siendo tediosos de probar. Desde Spring 3.2, hay una forma simplificada de hacer estas pruebas:

  • Asegúrese de que se carga un WebApplicationContext para la prueba anotando su clase de prueba con @WebAppConfiguration.

  • Inserte el mock de la consulta o sesión en su instancia de prueba y prepare el juego de pruebas.

  • Llame al componente web que ha recuperado del WebApplicationContext configurado (es decir, mediante la inserción de dependencias).

  • Haga las aserciones desde los mocks.

Configurar los beans de scope request

Por ejemplo, se crea una instancia de loginAction usando expresiones SpEL que recuperan el nombre de usuario y la contraseña de la consulta HTTP actual. En nuestra prueba, vamos a configurar estos argumentos de consulta a través del mock administrado por el framework TestContext.

<beans> 
        <bean id="userService" 
        class="com.example.SimpleUserService" 
        c:loginAction-ref="loginAction" /> 
        <bean id="loginAction" class="com.example.LoginAction" 
        c:username="{request.getParameter('user')}" 
                    c:password="{request.getParameter('pswd')}"  
        scope="request"> 
        <aop:scoped-proxy /> 
        </bean> 
        </beans> 

b. Pruebas de beans de scope request

En RequestScopedBeanTests, inyectamos el método LoginUser() que se invoca en nuestro userService. Estamos seguros de que el servicio del usuario tiene acceso al bean de scope request loginAction para el mock. A continuación, podemos completar los datos conocidos para el nombre de usuario y la contraseña.

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration 
        @WebAppConfiguration 
        public class RequestScopedBeanTests { 
            @Autowired UserService userService; 
            @Autowired MockHttpServletRequest request; 
            @Test 
        public void requestScope() { 
        request.setParameter("user", "enigma"); 
        request.setParameter("pswd", "$pr!ng"); 
                LoginResults results = userService.loginUser(); 
        // assert results 
            } 

c. Configuración de un bean de scope session

El bean userService depende de un bean de scope session userPreferences. Tenga en cuenta que el bean UserPreferences se instancia usando una expresión SpEL que recupera el tema de la sesión HTTP actual. En nuestra prueba, necesitaremos configurar un tema para el mock de la sesión administrada por el framework TestContext.

<beans> 
        <Bean  id = "userService" 
        clase = "com.example.SimpleUserService" 
        c: UserPreferences-ref = "PreferenciasUsuario" /> 
        <bean  id = "userPreferences" 
        class = "com.example.UserPreferences" 
        c:theme = "#{session.getAttribute( 'theme ')}"  
        scope = "session"> 
        <Aop: scope-proxy /> 
        </ bean> 
        </ Beans>  

Bean de scope session

Prueba asociada:

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration 
        @WebAppConfiguration 
        public class SessionScopedBeanTests { 
            @Autowired UserService userService; 
            @Autowired MockHttpSession session; 
            @Test 
        public void sessionScope() throws Exception { 
        session.setAttribute("theme", "Terminator"); 
                Results results = userService.processUserPreferences(); 
        // assert results 
        } 

6. Las transacciones

Las transacciones son importantes para las pruebas. ¿Queremos conservar la base de datos tal y como está o guardar los cambios una vez completadas las pruebas?

a. Gestión de transacciones Transacciones

En el framework TestContext, la clase TransactionalTestExecutionListener gestiona declarativamente las transacciones. Esta clase está configurada de forma predeterminada, incluso si no declara explícitamente TestExecutionListeners en la clase de prueba.

Para habilitar el soporte para las transacciones, debe configurar un bean PlatformTransactionManager en ApplicationContext, cargado por la semántica del @ContextConfiguration. Además, debe declarar la anotación Spring @Transactional a nivel de clase o de un método para sus pruebas.

b. Transacciones administradas por la prueba

Las transacciones administradas por las pruebas son operaciones que se controlan de manera declarativa a través de TransactionalTestExecutionListener o mediante programación a través de TestTransaction (ver a continuación). Estas operaciones no se deben confundir con las transacciones controladas directamente por Spring en ApplicationContext cargado para las pruebas, o con las transacciones de aplicación administradas mediante programación en el código de la aplicación invocado por las pruebas.

Las transacciones administradas por Spring o por la aplicación participan en transacciones controladas por pruebas. A nivel de la propagación de transacciones, es necesario estar atento a si las transacciones no están en modo REQUIRED o SUPPORTS.

Como se mencionó anteriormente, Spring puede anular transacciones para devolver la base de datos a su estado original, pero solo puede hacerlo para transacciones anidadas. Si parte del código está en una transacción aislada no gestionada por las pruebas, la actualización se realizará normalmente, lo que alterará el juego de pruebas.

c. Activación y desactivación de las transacciones

Anotar un método de prueba con @Transactional hace que la prueba se ejecute en una transacción que se anula automáticamente después de que se complete la prueba.

Si una clase de prueba está anotada con @Transactional, cada método de prueba dentro de esa jerarquía de clases se ejecutará en una transacción. Los métodos de prueba que no estén anotados con @Transactional (a nivel de clase o del método) no se ejecutarán en una transacción. Además, las pruebas que están anotadas con @Transactional, pero tienen el tipo de propagación NOT_SUPPORTED, no se ejecutarán en una transacción.

Tenga en cuenta que la clase AbstractTransactionalJUnit4SpringContextTests está preconfigurada para un soporte transaccional a nivel de clase.

En el siguiente ejemplo, se muestra un escenario común para escribir una prueba de integración para una base de datos de Hibernate UserRepository.

La base de datos se devuelve a su estado original después de la prueba a través de un rollback. De forma predeterminada, la transacción se anula automáticamente a través de un rollback al final de la prueba.

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration(clases = TestConfig.class)  
        @Transactional 
        public class HibernateUserRepositoryTests { 
            @Autowired 
            HibernateUserRepository repository; 
            @Autowired 
            SessionFactory sessionFactory; 
            JdbcTemplate jdbcTemplate; 
            @Autowired 
        public void setDataSource(DataSource dataSource) { 
                this.jdbcTemplate = new JdbcTemplate(dataSource); 
            } 
            @Test 
        public void createUser() { 
                // track initial state in test database: 
        final int count = countRowsInTable("user"); 
                User user = new User(...); 
        repository.save(user); 
                // Manual flush is required to avoid false positive 
        in test 
        sessionFactory.getCurrentSession().flush(); 
        assertNumUsers(count + 1); 
            } 
        protected int countRowsInTable(String tableName) return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, 
        tableName); 
            } 
        protected void assertNumUsers(int expected) { 
        assertEquals("Number of rows in the 'user' table.", expected, 
        countRowsInTable("user")); 
        } 

d. Comportamiento del commit y del rollback de una transacción rollback

De forma predeterminada, las operaciones de prueba se anulan automáticamente una vez completada la prueba. Sin embargo, este comportamiento se puede ajustar con la anotación @TransactionConfiguration en la clase o la anotación @Rollback en el método.

Gestión de transacciones basada en código

Desde Spring Framework 4.1, es posible interactuar con transacciones de prueba gestionadas por programación a través de métodos estáticos en TestTransaction.

Por ejemplo, TestTransaction se puede utilizar en métodos de prueba antes de los métodos y después de los métodos, para empezar o terminar la transacción de prueba actual o para solicitar a la transacción de prueba actual que realice una anulación o validación.

El soporte para TestTransaction está disponible automáticamente cuando se habilita TransactionalTestExecutionListener.

En el siguiente ejemplo se ilustran algunas de las características de TestTransaction.

@ContextConfiguration(clases = TestConfig.class) 
        public class ProgrammaticTransactionManagementTests extends  
        AbstractTransactionalJUnit4SpringContextTests { 
            @Test 
        public void transactionalTest() { 
                // assert initial state in test database: 
        assertNumUsers(2); 
        deleteFromTables("user"); 
                // changes to the database will be committed!  
        TestTransaction.flagForCommit();   
        TestTransaction.end(); 
        assertFalse(TestTransaction.isActive()); 
        assertNumUsers(0);  
        TestTransaction.start(); 
        [...] 
            } 
        protected void assertNumUsers(int expected) { 
        assertEquals("Number of rows in the 'user' table.", expected, 
        countRowsInTable("user")); 
        } 

e. Ejecutar código fuera de una transacción

De manera ocasional, es necesario ejecutar determinado código antes o después de un método de prueba transaccional, pero fuera del contexto transaccional, para comprobar el estado de una base de datos inicial antes de ejecutar su prueba o para verificar un commit planificado.

TransactionalTestExecutionListener soporta las anotaciones @BeforeTransaction y @AfterTransaction exactamente para estos escenarios.

Simplemente, anote un método de tipo public void en su clase de prueba con una de estas anotaciones y TransactionalTestExecutionListener se asegurará de que su método de transacción antes o después del método de la transacción se ejecute en el momento adecuado.

f. Configurar un administrador de transacciones Transacciones:administrador

TransactionalTestExecutionListener espera a que se defina un bean PlatformTransactionManager en ApplicationContext para realizar la prueba. En caso de que haya varias instancias de PlatformTransactionManager dentro de ApplicationContext, TransactionConfiguration soporta la configuración del nombre del bean de PlatformTransactionManager que se debe usar para realizar operaciones.

Alternativamente, podemos declararlo a través de @Transactional ("myQualifier"), y TransactionManagementConfigure se puede implementar mediante una clase Configuration.

g. Mostrar todas las anotaciones relacionadas con la transacción

El siguiente ejemplo, basado en JUnit, muestra un ejemplo del uso de las transacciones en las pruebas.

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration  
        @TransactionConfiguration(transactionManager="txMgr", 
        defaultRollback=false@Transactional 
        public class TransactionFictiveTest {  
        @BeforeTransaction 
        public void verifyInitialDatabaseState() { 
        [...] 
            }  
        @Before 
        public void setUpTestDataWithinTransaction() { 
        [...] 
            }  
        @Rollback(true) 
        public void modifyDatabaseWithinTransaction() { 
        [...] 
            }  
        @After 
        public void tearDownWithinTransaction() { 
        [...] 
            }  
        @AfterTransaction 
        public void verifyFinalDatabaseState() { 
        [...] 
        } 

Con Hibernate, es necesario eliminar la caché para evitar falsos positivos.

@Autowired 
        private SessionFactory sessionFactory; 
        @Test public void falsoPositivo() { 
        updateEntityInHibernateSession();  
        // Falso positivo 
        @Test(expected = GenericJDBCException.class) 
        public void updateWithSessionFlush() { 
        updateEntityInHibernateSession();  
        // Vaciamos la caché para tener los datos actualizados. 
        sessionFactory.getCurrentSession().flush(); 
        // ... 

7. Los scripts SQL SQL

Spring permite especificar el formato de la base de datos y los datos que se inyectarán en la base de datos en forma de scripts SQL. Normalmente, se debe evitar SQL en un programa utilizando Hibernate o JPA u otro ORM para manejar el nivel de abstracción física de los datos. Sin embargo, en la práctica, es habitual que la base de datos sea conocida y muchas bases de datos son emuladas por H2, por ejemplo, y esto posibilita hacer SQL. Todo depende de los casos de uso y la energía que pongamos en los problemas de prueba. SQL suele ser suficiente para un proyecto que tiene un mapping uno a uno entre sus objetos y la base de datos (una clase mapeada por una tabla). Para una aplicación que utiliza las cuatro estrategias de mapping Hibernate: una tabla por jerarquía de clases, una tabla por subclase, una tabla por clase concreta y el polimorfismo implícito, las cosas se pueden complicar cuando se crean juegos de pruebas SQL. Finalmente, para una aplicación que comparte su base de datos con otras aplicaciones, SQL se vuelve casi obligatorio.

a. Ejecutar scripts SQL

Como hemos visto, las pruebas unitarias involucran solo a una entidad de negocio o una clase y generalmente se realizan con mocks. Las pruebas de integración son pruebas que se centran en uno o más servicios en los que participan varias entidades de negocio. A continuación, es necesario crear juegos de prueba que superen las capacidades de los mocks. Para estas pruebas, los clústeres de objetos se crean sobre la marcha a través de llamadas a servicios o rellenando una base de datos con conjuntos de pruebas, que se crean desde cero o se extraen de una base de datos real.

Al escribir pruebas de integración con una base de datos relacional, el uso de scripts SQL suele ser práctico para cambiar el esquema de la base de datos o insertar datos de prueba en las tablas. Es posible hacerlo a mano o utilizando herramientas como DbUnit (http://dbunit.sourceforge.net/) o Liquibase (http://www.liquibase.org/).

El módulo Spring-jdbc proporciona soporte con la inicialización integrada de datos en una base de datos existente mediante la ejecución de scripts SQL cuando se carga el Spring ApplicationContext. Aunque es muy útil para inicializar una base de datos para probar mientras se carga ApplicationContext, algunas veces también es indispensable poder modificar la base de datos durante las pruebas de integración.

Ejecutar scripts SQL a través del código

Spring proporciona las siguientes opciones para ejecutar scripts SQL mediante programación, dentro de los métodos de prueba de integración.

  • org.springframework.jdbc.datasource.init.ScriptUtils

  • org.springframework.jdbc.datasource.init.ResourceDatabasePopulator

  • org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests

  • org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests

ScriptUtils proporciona una colección de métodos de utilidad estáticos para trabajar con scripts SQL y está destinado principalmente para uso interno en el framework. Sin embargo, si necesita un control completo sobre el análisis y la ejecución del SQL, ScriptUtils satisface sus necesidades mejor que otras alternativas que se describen a continuación. ScriptUtils

ResourceDatabasePopulator proporciona una API orientada a objetos sencilla para manipular la base de datos mediante scripts SQL definidos en los recursos externos. ResourceDatabasePopulator

ResourceDatabasePopulator proporciona opciones para configurar la codificación de caracteres, los separadores de declaración, los separadores de comentarios y la manipulación de los flags de error utilizados cuando se analizan y ejecutan scripts, y cada una de las opciones de configuración tiene un valor predeterminado. ResourceDatabasePopulator

Para ejecutar scripts configurados en un ResourceDatabasePopulator, puede invocar el método populate(Connection) para ejecutar el provisioning a partir de un java.sql.Connection o el método execute(DataSource) para ejecutar el provisioning desde un javax.sql.DataSource

Por ejemplo, para un esquema y datos de prueba, con un separador de declaración como "@@", con ejecución de script desde un DataSource:

@Test 
        public void databaseTest { 
            ResourceDatabasePopulator populator = 
        new ResourceDatabasePopulator(); 
        populator.addScripts( 
        new ClassPathResource("test-schema.sql"), 
        new ClassPathResource("test-data.sql"));  
        populator.setSeparator("@@"); 
        populator.execute(this.dataSource); 
             // execute code that uses the test schema and data 

Tenga en cuenta que el ResourceDatabasePopulator interno delega el análisis y la ejecución de scripts SQL a la clase ScriptUtils.

Del mismo modo, los métodos executeSqlScript(..) utilizan un ResourceDatabasePopulator para ejecutar scripts.

Ejecutar scripts SQL declarativamente con SQL

Los scripts SQL también se pueden configurar de forma declarativa en el framework Spring TestContext. En concreto, se puede declarar la anotación @Sql en un método o clase de prueba para configurar las rutas de acceso de recursos a los scripts SQL que se deben ejecutar en una base de datos antes o después de un método de prueba de integración.

Tenga en cuenta que las declaraciones a nivel del método sobrecargan las declaraciones a nivel de la clase y el soporte de @Sql lo ofrece el listener SqlScriptsTestExecutionListener, que está habilitado de forma predeterminada. SqlScriptsTestExecutionListener

b. La semántica del path de los recursos Path

Por ejemplo, "schema.sql" se trata como un recurso de classpath relativo al paquete en el que se define la clase de prueba. Una ruta que comienza con un slash se tratará como un classpath, como en este ejemplo: "/org/example/schema.sql". Una ruta que haga referencia a una dirección URL (por ejemplo, una ruta con el prefijo classpath:, file:, http:, etc.) se cargará utilizando el protocolo de recursos especificado.

En el siguiente ejemplo, se muestra cómo utilizar SQL a nivel de clase y del método en una clase de prueba de integración basada en JUnit:

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration  
        @Sql("/test-schema.sql") 
        public class DatabaseTests { 
            @Test  
            public void emptySchemaTest { 
                [...] 
            } 
            @Test  
            @Sql({"/test-schema.sql", "/test-user-data.sql"}) 
            public void userTest { 
                [...] 
            } 

c. Detección predeterminada de scripts

Si no se especifica ninguno de los scripts SQL, se intentará detectar un script predeterminado en función de dónde se declara la anotación @Sql.

Spring genera una excepción IllegalStateException si no puede encontrar un archivo. Para facilitar el mantenimiento, podemos añadir un comentario en el código que indique que estamos utilizando ubicaciones implícitas.

  • Declaración a nivel de clase: si la clase de prueba anotada es com.example.MiTest, el script predeterminado correspondiente es "classpath:com/example/MiTest.sql".

  • Declaración a nivel del método: si el método de prueba anotado se denomina testMethod() y se define en la clase com.example.MiTest, el script predeterminado correspondiente es "classpath:com/example/MiTest.testMethod.sql".

d. Declarar varios @Sql

Si es necesario configurar varios conjuntos de scripts SQL para una clase de prueba o para un método de prueba determinados, pero con una configuración diferente de la sintaxis, reglas de tratamiento de errores diferentes o diferentes fases de ejecución, es posible declarar varias instancias de SQL.

Con Java 8, @Sql se puede utilizar como una anotación «repetible». En caso contrario, la anotación @SqlGroup se puede utilizar como una anotación «contenedor» para declarar varias instancias @Sql.

En el siguiente ejemplo, se muestra el uso de @Sql como anotación repetible utilizando Java 8. En este escenario, el script test-schema.sql utiliza una sintaxis diferente para los comentarios de una sola línea.

@Test  
        @Sql(scripts = "/test-schema.sql", config = 
        @SqlConfig(comoPrefijo = "`")) 
        @Sql("/test-user-data.sql") 
        public void userTest { 
        [...] 

El siguiente ejemplo es idéntico al anterior, excepto que las declaraciones @Sql se agrupan en un @SqlGroup para la compatibilidad con Java 6 y Java 7.

@Test  
        @SqlGroup({ 
        @Sql(scripts = "/test-schema.sql", config = 
        @SqlConfig(comoPrefijo = "`")), 
        @Sql("/test-user-data.sql") )} 
        public void userTest { 
        [...] 

e. Fases de ejecución de los scripts

De forma predeterminada, los scripts SQL se ejecutan antes que el método de prueba correspondiente. Sin embargo, si se debe ejecutar un conjunto de scripts después del método de prueba, por ejemplo, para limpiar el estado de la base de datos, se puede usar el atributo executionPhase de la anotación @Sql. Tenga en cuenta que ISOLATED y AFTER_TEST_METHOD se importan estáticamente desde Sql.TransactionMode y Sql.ExecutionPhase, respectivamente. ISOLATED AFTER_TEST_METHOD

@Test 
        @Sql( 
        scripts = "create-test-data.sql", 
        config = @SqlConfig(transactionMode = ISOLATED@Sql( 
        scripts = "delete-test-data.sql", 
        config = @SqlConfig(transactionMode = ISOLATED), 
        executionPhase = AFTER_TEST_METHOD 
        public void userTest { 
        [...] 

f. Script de configuración con SqlConfig

La configuración para el análisis de scripts y la manipulación de errores se puede configurar mediante la anotación @SqlConfig.

Cuando se declara como una anotación en la clase de prueba de integración, @SqlConfig sirve como configuración global para todos los scripts SQL dentro de la jerarquía de clases de prueba.

Cuando se declara directamente a través del atributo config de la anotación @Sql, @SqlConfig sirve como configuración local para los scripts SQL declarados dentro de la anotación @Sql. Cada atributo en @SqlConfig tiene un valor predeterminado.

Debido a las reglas definidas para los atributos de anotación en la especificación del lenguaje Java, desafortunadamente no es posible asignar un valor null a un atributo de la anotación. Por lo tanto, para admitir sustituciones de configuración global heredada, los atributos de @SqlConfig tienen un valor predeterminado explícito cadena vacía para Spring o DEFAULT para las enumeraciones.

Este enfoque permite que las declaraciones locales de @SqlConfig sobrecarguen selectivamente los atributos individuales de las declaraciones globales de @SqlConfig, proporcionando un valor distinto de cadena vacía o DEFAULT. Los atributos globales se heredan para cada atributo @SqlConfig local que no proporciona un valor explícito distinto de cadena vacía o DEFAULT. Por lo tanto, la configuración local explícita sustituye a la configuración global.

Las opciones de configuración proporcionadas por @Sql y @SqlConfig son equivalentes a las admitidas por ScriptUtils y ResourceDatabasePopulator, pero son un superconjunto de las proporcionadas por el espacio de nombres XML <jdbc:initialize-database/>.

g. Gestión de transacciones para @Sql

Es difícil implementar las pruebas que verifican el correcto funcionamiento transaccional en los diferentes usos del componente probado. De forma predeterminada, SqlScriptsTestExecutionListener inferirá la semántica de transacción deseada para su script configurado mediante @Sql. SqlScriptsTestExecutionListener

De manera más precisa, los scripts SQL se pueden ejecutar fuera de las transacciones o en una transacción existente administrada por Spring, como por ejemplo por TransactionalTestExecutionListener para una prueba anotada con @Transactional, o en una operación aislada, en función del valor configurado del atributo ModeTransaction de @SqlConfig y la presencia de un PlatformTransactionManager en ApplicationContext. TransactionalTestExecutionListener

Al menos debe haber un javax.sql.DataSource en la prueba de ApplicationContext. Proporciona los argumentos de conexión a la base de datos utilizada para las pruebas.

Si los algoritmos que usan SqlScriptsTestExecutionListener para detectar un DataSource y PlatformTransactionManager para inferir la semántica de transacciones no se adaptan a sus necesidades, puede especificar nombres explícitos para estos elementos a través de los atributos dataSource y transactionManager de @SqlConfig.

Además, el comportamiento de propagación de la transacción se puede controlar a través del atributo ModeTransaction de @SqlConfig, si su ejecución se va a realizar en una transacción aislada.

Ejemplo de TransactionalSqlScriptsTests:

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration(clases = TestDatabaseConfig.class) 
        @Transactional 
        public class TransactionalSqlScriptsTests { 
            protected JdbcTemplate jdbcTemplate; 
            @Autowired 
            public void setDataSource(DataSource dataSource) { 
              this.jdbcTemplate = new JdbcTemplate(dataSource); 
            } 
            @Test 
            @Sql("/test-data.sql") 
            public void usersTest() { 
              [...] 
              assertNumUsers(2); 
              [...] 
            } 
            protected int countRowsInTable(String tableName) { 
              return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, 
        tableName); 
            } 
            protected void assertNumUsers(int expected) { 
              assertEquals("Number of rows in the 'user' table.", 
        expected, 
              countRowsInTable("user")); 
            } 
        } 

8. Clases de soporte TestContext TestContext

a. Clases de soporte de JUnit

El paquete org.springframework.test.context.junit4 proporciona las siguientes clases de soporte para casos de prueba basados en JUnit 4.

  • ResumenJUnit4SpringContextTests

  • ResumenTransaccionalJUnit4SpringContextTests

AbstractJUnit4SpringContextTests es una clase de prueba básica abstracta que integra el framework Spring TestContext con un soporte de pruebas explícito para ApplicationContext en un entorno JUnit. AbstractJUnit4SpringContextTests

Al extender AbstractJUnit4SpringContextTests, puede acceder a una variable de instancia Protected ApplicationContext applicationContext que se puede usar para realizar búsquedas de beans explícitos o para probar el estado del contexto en su conjunto.

public class ContextJUnitTest extends AbstractJUnit4SpringContextTests { 
            @Test 
            public void testContext() { 
                Assert.assertNotNull(applicationContext.getBean("test")); 
            } 
        } 

AbstractTransactionalJUnit4SpringContextTests es una extensión transaccional abstracta de AbstractJUnit4SpringContextTests que facilita el acceso JDBC. Requiere que se definan unos bean javax.sql.DataSource y PlatformTransactionManager en el contexto ApplicationContext. AbstractTransactionalJUnit4SpringContextTests

Al extender AbstractTransactionalJUnit4SpringContextTests, puede acceder a una variable de instancia protectedjdbcTemplate que se puede utilizar para ejecutar instrucciones SQL a fin de consultar la base de datos. Estas consultas se pueden utilizar para confirmar el estado de la base de datos antes y después de ejecutar el código de la base de datos de la aplicación. AbstractTransactionalJUnit4SpringContextTests también proporciona métodos prácticos que delegan a los métodos de JdbcTestUtils, los cuales utilizan el JdbcTemplate citado anteriormente.

Además, AbstractTransactionalJUnit4SpringContextTests proporciona un executeSqlScript(..) para ejecutar scripts SQL en el DataSource configurado.

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration({"classpath:test-services-spring-context.xml"}) 
        @TransactionConfiguration(transactionManager =  
        "vehiculoTransactionManager"public class VehiculoDAOIT extends AbstractTransactional 
        JUnit4SpringContextTests { 
           @Autowired 
           private VehiculoDAO vehiculoDAO; 
         
           @Before 
           public void setUp(){  
             executeSqlScript("classpath:testdata/sql/Vehiculo_Create.sql"false); 
           } 
        } 

b. Spring JUnit Runner Runner

El framework TestContext Spring ofrece una integración completa con JUnit 4.9+, a través de un ejecutor personalizado (probado en JUnit 4.9 a 4.11).

Al anotar clases de prueba con @RunWith (SpringJUnit4ClassRunner.class), los desarrolladores pueden implementar pruebas unitarias y de integración basadas en el JUnit estándar y, al mismo tiempo, disfrutar de las ventajas del framework TestContext, como el soporte para los contextos de aplicación de carga, inyección de dependencias de instancias de prueba, ejecución de métodos de prueba transaccionales y así sucesivamente. El siguiente código muestra los requisitos mínimos para configurar una clase de prueba para trabajar con el Spring personalizado.

El runner TestExecutionListeners está configurado con una lista vacía para deshabilitar los listeners de forma predeterminada, lo que de otro modo requeriría un ApplicationContext y ser configurados por un ContextConfiguration.

@RunWith(SpringJUnit4ClassRunner.class) 
        @TestExecutionListeners({}) 
        public class SimpleTest { 
            @Test 
            public void testMethod() { 
            [...] 
        } 

9. Framework Spring MVC Test

a. Proyecto autónomo

En este capítulo se proporcionan detalles sobre la migración de proyectos Spring 3. De hecho, esta versión ya no será compatible con Spring, aunque todavía hay grandes activos de pruebas que dependen de esta versión. De hecho, había un framework de pruebas Spring MVC en esta versión.

Este framework de pruebas Spring MVC ya existía antes de que se incluyera en Spring Framework 3.2 como un proyecto separado en GitHub. Ha crecido y evolucionado gracias a su uso y se ha beneficiado de múltiples contribuciones.

Este proyecto autónomo de framework de pruebas Spring MVC todavía está disponible en GitHub y se puede usar junto con Spring Framework 3.1.x. Las aplicaciones actualizadas a Spring 3.2 o a una versión posterior deben sustituir las dependencias de Spring-test-mvc con una dependencia de spring-test.

El módulo Spring Test utiliza un paquete org.springframework.test.web que es casi idéntico, salvo por dos excepciones:

  • El soporte de nuevas funcionalidades de Spring 3.2 (por ejemplo, las consultas web asincrónicas).

  • La posibilidad de usar MockMvc.

En Spring 3.2 y versiones superiores, debe utilizar el framework TestContext, que ofrece servicios de almacenamiento en caché para la configuración cargada.

El framework Spring MVC Test proporciona soporte para JUnit para el cliente de pruebas. Se soporta el código del lado del servidor Spring MVC por medio de una API fluida.

Normalmente, carga la configuración Spring real a través del framework TestContext y siempre usa DispatcherServlet para procesar solicitudes. Esto lo acerca a las pruebas de integración completa sin requerir el inicio de un contenedor de servlets.

Las pruebas del lado cliente se basan en RestTemplate. Esto permite realizar pruebas de código que descansan en RestTemplate sin necesidad de que un servidor arranque para responder a las peticiones. RestTemplate

b. Pruebas del lado del servidor

Antes de Spring Framework 3.2, la forma más común de probar un controlador Spring MVC era escribir una prueba unitaria que creara instancias del controlador y lo inyectara con las dependencias en los mocks o stubs para, a continuación, llamar a sus métodos directamente utilizando un MockHttpServletRequest y un MockHttpServletResponse si es necesario. MockHttpServletRequest MockHttpServletResponse

Aunque resulta bastante sencillo de hacer, los controladores tienen numerosas anotaciones y muchas no están probadas. El mapping de peticiones, el Binding de datos, la conversión de tipos y la validación son solo algunos ejemplos de lo que no se prueba. Además, hay otros tipos de métodos anotados, como @InitBinder, @ModelAttribute y @ExceptionHandler, que se invocan en una parte del procesamiento de la consulta.

La idea detrás de las pruebas Spring MVC es poder reescribir estas pruebas de controlador haciendo solicitudes reales y generando respuestas, como en una ejecución real, invocando los controladores a través del DispatcherServlet de Spring MVC.

Los controladores también se pueden inyectar con dependencias en los mocks y, por lo tanto, pueden permanecer concentrados en la capa web. El framework de pruebas Spring MVC se basa en las implementaciones familiares de mocks de la API Servlet, disponibles en el módulo spring-test. Esto permite la ejecución de request y la producción de respuestas sin que sea necesario tener un contenedor de servlets para su ejecución. Para la mayoría de los procesos, todo debería funcionar como lo haría en tiempo de ejecución, excepto para renderizar las JSP, que no están disponibles fuera de un contenedor de Servlet.

Además, si está familiarizado con el funcionamiento del mock MockHttpServletResponse, sabrá que los forwards y las redirections no se ejecutan realmente.

En su lugar, se registran las URL "forwarded" y "redirected" y se pueden invocar en las pruebas. Esto significa que, si está utilizando JSP, puede consultar la página JSP a la que se reenvió la solicitud. El resto de las formas de representación, incluidos los métodos @ResponseBody y los tipos View (además de JSP) como Freemarker, Velocity, thymeleaf y otros motores de templating para representar HTML, JSON, XML, etc., deben funcionar como está previsto, y la respuesta incluirá el contenido generado.

He aquí un ejemplo de prueba de una solicitud de información en una cuenta en formato JSON:

@RunWith(SpringJUnit4ClassRunner.class) 
        @WebAppConfiguration 
        @ContextConfiguration("test-servlet-context.xml"public  class ExampleTests { 
         @Autowired 
         private WebApplicationContext wac; 
         privateMockMvc mockMvc; 
         @Before 
         public  vide setup () { 
           this.mockMvc = MockMvcBuilders.webAppContextSetup ( 
           this..wac).build (); 
         } 
         @Test 
         publicvoid getAccount () throw Exception { 
           this.mockMvc.perform(get( "/accounts/1" 
         ).accept(MediaType.parseMediaType( "application/json;charset=UTF-8" ))) 
                    .andExpect (statut (). IsOk ()) 
                    .andExpect (content (). contentType ( "application / json" )) 
                    .andExpect (jsonPath ( "$ .name" ) .value ( "Lee" )); 
        } 

La prueba se basa en la compatibilidad con WebApplicationContext del framework TestContext. WebApplicationContext TestContext

Carga la configuración de Spring desde un archivo de configuración XML, ubicado en el mismo paquete que la clase de prueba, e inyecta el WebApplicationContext creado en la prueba, lo que hace que una instancia de mock MockMvc se pueda crear con él.

A continuación, MockMvc se utiliza para realizar una solicitud de "/accounts/1" para comprobar que el estado de la respuesta resultante es 200, que el tipo de contenido de la respuesta es "application/json" y que el contenido de la respuesta tiene una propiedad JSON, denominada "nombre" con el valor "Lee".

En este ejemplo, el contenido JSON se inspecciona con ayuda del proyecto JsonPath Jayway (https://github.com/json-path/JsonPath).

Hay muchas otras opciones para verificar el resultado de la solicitud realizada.

La API fluida en el ejemplo anterior requiere algunas importaciones estáticas, como:

  • MockMvcRequestBuilders.*

  • MockMvcResultMatchers.*

  • MockMvcBuilders.*

El objetivo de la configuración de prueba del lado del servidor es crear una instancia de MockMvc que se pueda usar para realizar requests.

Hay dos opciones principales:

  • Configurar la configuración en Spring MVC a través del framework TestContext, que carga la configuración de Spring e inyecta un WebApplicationContext en la prueba que se utilizará para crear un MockMvc:

@RunWith(SpringJUnit4ClassRunner.class) 
        @WebAppConfiguration 
        @ContextConfiguration("my-servlet-context.xml"public  class MyWebTests { 
         @Autowired 
         private WebApplicationContext wac; 
         privateMockMvc mockMvc; 
         pubic  vide setup () { 
          this.mockMvc = MockMvcBuilders.webAppContextSetup 
            (this.wac).build (); 
          } 
            // ... 
  • Registrar una instancia de controlador sin cargar toda la configuración de Spring:

public class MyWebTests { 
            private MockMvc mockMvc; 
            @Before 
            public void setup() { 
                this.mockMvc = MockMvcBuilders.standaloneSetup(new 
            AccountController()).build(); 
            } 
            // ... 

webAppContextSetup carga la configuración real de Spring MVC, lo que da como resultado una prueba de integración más completa. TestContext almacena en caché la configuración, lo que ayuda a mantener las pruebas rápidas incluso si se añaden más.

Además, puede inyectar servicios mokeados en los controladores a través de la configuración de Spring para que se pueda concentrar en probar la capa web.

A continuación, se muestra un ejemplo de declaración de un servicio mokeado con Mockito:

<bean id="accountService" class="org.mockito.Mockito" factory- 
        method="mock"<constructor-arg value="org.example.AccountService"/> 
        </bean> 

Seguidamente, puede inyectar el servicio mokeado en la prueba para verificar las expectativas:

@RunWith(SpringJUnit4ClassRunner.class) 
        @WebAppConfiguration 
        @ContextConfiguration("test-servlet-context.xml"public  class AccountTests { 
            @Autowired 
            private WebApplicationContext wac; 
            private MockMvc mockMvc; 
            @Autowired 
            privateAccountService AccountService; 
        // ... 

Por otro lado, el standaloneSetup se parece más a una prueba unitaria. Prueba un controlador a la vez. El controlador se puede inyectar con dependencias mocks manualmente y no tiene una configuración Spring de carga. Estas pruebas se centran más en la forma y facilitan la detección del controlador que se está probando o el control de una configuración específica Spring MVC, etc.

El uso de un standaloneSetup, descrito en un ejemplo un poco más adelante en este capítulo, también es una forma muy cómoda de escribir pruebas «ad hoc», para comprobar determinados comportamientos o depurar un problema. Al igual que con la integración y las pruebas unitarias, no hay una forma correcta o incorrecta de hacerlo.

El uso del standaloneSetup no significa que algunas pruebas adicionales deban usar webAppContextSetup para comprobar la configuración Spring MVC.

Como alternativa, puede decidir escribir todas las pruebas con webAppContextSetup y seguir probando la configuración actual de Spring MVC.

Ejecución de consultas

Para realizar requests, utilice el método HTTP adecuado y los métodos de style builder correspondientes a las propiedades de MockHttpServletRequest. Por ejemplo:

mockMvc.perform(post("/hotels/{id}"42).accept(MediaType.APPLICATION_JSON)); 

Además de todos los métodos HTTP, también puede hacer pruebas sobre los archivos cargados, lo que crea una instancia interna de MockMultipart HttpServletRequest:

mockMvc.perform(fileUpload("/doc").file("a1""ABC".getBytes("UTF-8"))); 

Los argumentos de cadena de consulta se pueden especificar en el modelo de URI:

mockMvc.perform(get("/hotels? foo={foo}", "bar")); 

O añadiendo argumentos desde la consulta de Servlet:

mockMvc.perform(get("/hotels").param("foo", "bar")); 

Si el código de la aplicación se basa en los argumentos de las consultas Servlet y no comprueba la cadena de consulta, como suele ser el caso, no importa cómo se agreguen los argumentos. Tenga en cuenta que los argumentos proporcionados en el modelo URI ya estarán decodificados, mientras que los argumentos proporcionados por el método param(...) se tendrán que decodificar. En la mayoría de los casos, es mejor dejar intacto en la URI de consulta el path del contexto y del servlet.

Si tiene que probar con la URI de la request completa, asegúrese de establecer contextPath y servletPath en consecuencia para que el mapping de las requests funcione:

mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app"). 
        servletPath("/main")) 

Mirando el ejemplo anterior, sería engorroso ajustar contextPath y servletPath para cada consulta realizada.

Por este motivo, se pueden definir las propiedades predeterminadas de las requests al crear los MockMvc:

public class MyWebTests private MockMvc mockMvc; 
            @Before 
        public void setup() { 
            mockMvc = standaloneSetup(new AccountController()) 
                    .defaultRequest(get("/") 
                    .contextPath("/app").servletPath("/main") 
                    .accept(MediaType.APPLICATION_JSON).build(); 
        } 

Las propiedades anteriores se aplicarán a todas las solicitudes realizadas por MockMvc.

c. Definir expectativas

Las expectativas se pueden definir añadiendo uno o más .andExpect() después de las llamadas, para evaluar la respuesta:

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk()); 

El paquete MockMvcResultMatchers.* define una serie de miembros estáticos, algunos de cuyos tipos permiten, junto con otros métodos, verificar el resultado de la consulta realizada.

Las aserciones se dividen en dos categorías generales:

  • Comprobar las propiedades de la respuesta, es decir, el estado de la respuesta, las cabeceras y el contenido, que son las cosas más importantes que se deben probar.

  • Ir más allá de la respuesta y permitir la inspección de construcciones específicas de Spring MVC, como el método del controlador que manejó la solicitud, el lanzamiento y manejo de una excepción, el contenido del modelo, el punto de vista seleccionado o los atributos flash añadidos. También es posible verificar construcciones de servlets específicos, como la petición de sesión y de atributos. La siguiente prueba confirma que se produjo un error en el enlace/validación:

mockMvc.perform(post("/personas")) 
            .andExpect(status().isOk()) 
        .andExpect(model().attributeHasErrors("persona")); 

Al escribir pruebas, algunas veces es útil actualizar la base de datos, a través de un flush, con el resultado de la request realizada. Esto se puede hacer de la siguiente manera, donde print() es una importación estática de MockMvcResultHandlers: MockMvcResultHandlers

    .andDo(print()) 
            .andExpect(status().isOk()) 
            .andExpect(model().attributeHasErrors("persona")); 

Siempre que el procesamiento de la solicitud provoque una excepción no controlada, el método print() permite mostrar todos los datos de resultados disponibles en System.out.

En algunos casos, es posible que desee obtener acceso directo al resultado para verificar algo que no se puede verificar de otra manera. Esto se puede hacer añadiendo .andReturn() al final, después de todas las expectativas:

MvcResult mvcResult =  
        mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn(); 

Cuando todas las pruebas repiten las mismas aserciones, puede definir las aserciones comunes una vez al construir el MockMvc:

standaloneSetup(new SimpleController()) 
           .alwaysExpect(status().isOk()) 
           .alwaysExpect(content().contentType("application/json;charset=UTF-8")) 
           .build() 

Tenga en cuenta que la expectativa siempre se aplica y no se puede cancelar sin crear una instancia MockMvc independiente.

Detallaremos el módulo Spring HATEOAS en un capítulo específico, pero recordamos aquí cómo proceder para las pruebas.

Cuando el contenido de la respuesta JSON contiene hiperenlaces creados con Spring HATEOAS, se pueden comprobar los vínculos resultantes:

mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON) 
        )  
            .andExpect(jsonPath("$.links[?(@.rel ==  
        'self')].href").value("http://localhost:8080/people")); 

Cuando el contenido de la respuesta XML contiene hiperenlaces creados con Spring HATEOAS, se pueden comprobar los vínculos resultantes:

Map<String, String> ns = Collections.singletonMap("ns""http://www.w3.org/2005/Atom"); 
        mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML)) 
            .andExpect(xpath("/person/ns:link[@rel='self']/@href",  
        ns).string("http://localhost:8080/people")); 

d. Añadir filtros

Al configurar un MockMvc, puede registrar una o más instancias Filter:

mockMvc = standaloneSetup(new PersonController()).addFilters(new 
        CharacterEncodingFilter()).build(); 

Los filtros guardados serán invocados por el MockFilterChain de Spring y el último filtro se delegará al DispatcherServlet.

Los TU y TI del framework Spring contienen una multitud de pruebas de muestra.

e. Pruebas REST del lado cliente REST

Las pruebas del lado cliente usan RestTemplate. El objetivo es definir las request esperadas y proporcionar los «stubs» de las respuestas:

RestTemplate restTemplate = new RestTemplate(); 
        MockRestServiceServer mockServer = 
        MockRestServiceServer.createServer(restTemplate); 
        mockServer.expect(requestTo("/greeting")).andRespond(withSuccess( 
        "Hello world", MediaType.TEXT_PLAIN)); 
        // use RestTemplate ... 
        mockServer.verify(); 

En el ejemplo anterior, la clase principal para las pruebas de REST del lado cliente MockRestServiceServer configura RestTemplate con un ClientHttpRequestFactory personalizado, que comprueba las requests reales con las expectativas y, a cambio, proporciona «stubs» para las respuestas. MockRestServiceServer ClientHttpRequestFactory

En este caso, esperamos una sola solicitud de "/greeting" y una respuesta 200 con contenido "text/plain".

Una vez que se han definido las consultas y respuestas esperadas de derivación, RestTemplate se puede usar en el código del lado cliente como de costumbre. Al final de la prueba, mockServer.verify() se puede utilizar para comprobar que se han servido todas las requests.

10. Otros recursos

Consulte los siguientes recursos para obtener más información acerca de las pruebas:

JUnit: un framework de pruebas orientado al programador para Java que el framework Spring utiliza en su conjunto de pruebas.

https://junit.org/junit4/

MockObjects.com: sitio web dedicado a mokear objetos, una técnica para mejorar el diseño de código en el desarrollo basado en pruebas.

Mockito: librería Java de mocks basada en el espionaje de patrones de prueba.

https://site.mockito.org/

DbUnit: extensión de JUnit (también utilizable con Ant y Maven) dirigida a proyectos basados en bases de datos. Entre otras cosas, esto coloca su base de datos en un estado conocido entre cada prueba.

http://dbunit.sourceforge.net/

Puntos clave

  • Las pruebas son fáciles de implementar con Spring.

  • Se deben realizar todas las pruebas necesarias para facilitar el mantenimiento.

  • Spring permite probar en un entorno muy ligero.

  • Las pruebas de Spring tienen muy buen rendimiento.

Descripción del problema

En este capítulo se presenta la parte back de una aplicación con una base de datos SQL. Un capítulo específico se ocupará del equivalente con las bases de datos NoSQL. Usamos SQL con aplicaciones que dependen de una base de datos históricamente SQL o con aplicaciones que cambiarán poco. De hecho, la complejidad de una aplicación basada en una base de datos relacional se debe comparar con el uso directo de una base de datos NoSQL.

Para una base de datos SQL, un ORM (Object Relational Mapping) es una herramienta que permite pasar de un modelo lógico orientado a objetos a un modelo físico relacional. Hay varios, como Hibernate, MyBatis, pero el más utilizado es Hibernate, que a menudo se combina con JPA. ORM Hibernate

JPA es una API Java que se basa en una implementación de un ORM; usamos JPA con Hibernate. Hibernate añade funcionalidades adicionales. Las anotaciones y las API Hibernate solo se utilizan si las funcionalidades que necesita no están disponibles en JPA. JPA

El ORM es el enlace entre la base de datos y los objetos de Java de más bajo nivel, que se agrupan en la capa llamada «capa de dominio de negocio».

Para casos sencillos, el ORM administra el SQL para intercambios con la base de datos, pero es posible personalizar el SQL para casos complejos. En estos casos,  nos abstraemos totalmente del tipo de base de datos. Por ejemplo, es posible hacer pruebas en una base de datos de memoria como H2 y tener una base de datos de producción DB2, Oracle o Sybase, sin tocar el código Java.

Los ORM modernos también ofrecen funcionalidades adicionales, como la auditoría y el control de versiones de los datos (Envers), la provisión de estadísticas sobre el uso que se hace de los datos o sobre el rendimiento. La implementación de una caché simple o compartida también permite tener ganancias significativas en el rendimiento si la aplicación está bien pensada.

Hacer la correspondencia entre un modelo de objetos que, por definición, soporta la herencia y el polimorfismo en un modelo relacional basado en tablas planas no es sencillo. Los ejemplos del libro, en aras de la simplificación, solo usarán el mapping más sencillo: una tabla correspondiente a un objeto Java de dominio.

ORM también hace que las relaciones entre objetos sean transparentes. Estas asociaciones de objetos son omnidireccionales o bidireccionales.

Tipo de relaciones entre clases

  • One-to-one: un solo objeto que apunta a otro objeto único.

  • Many-to-one: un conjunto de objetos que apuntan a objeto único.

  • One-to-many: un objeto que apunta a un conjunto de objetos.

  • Many-to-many: un conjunto de objetos que apuntan a otro conjunto de objetos. 

Usamos el atributo mappedBy para identificar la foreign key.

ORM hace la correspondencia entre los tipos Java y los tipos de columna de las tablas de la base de datos. ORM también tiene como objetivo reflejar las restricciones sobre las tablas en los objetos de dominio y viceversa.

Históricamente, el primer ORM Java fue TopLink, pero su uso era de pago. Para armonizar los desarrollos cliente/servidor Java, SUN (ahora Oracle) propuso una arquitectura J2EE basada en EJB con diferentes formas de mapear objetos Java y bases de datos. Utilizar EJB 1.0 y posteriormente EJB 2.0 era laborioso. Más adelante se creó Hibernate, un poco antes de Spring, para simplificar las cosas. Posteriormente, Java integró los conceptos Hibernate y TopLink en la versión enterprise (Java EE) de Java, pasando a los EJB3, que son más fáciles de implementar, y creando la primera versión de la API JPA 1.0. Más tarde, TopLink se convirtió en EclipseLink y JPA continuó evolucionando en paralelo con Hibernate. A menudo elegirá Hibernate como su implementación para JPA.

Al igual que sucede con Spring, es posible configurar JPA e Hibernate a través de archivos XML y anotaciones. Hoy en día, las anotaciones se utilizan tanto como sea posible, manteniendo solo una pequeña parte de la configuración en .xml y .properties.

La complejidad inherente al uso de un ORM está relacionada con la evolución del modelo de datos. Al principio de un proyecto, el modelo suele ser sencillo y todo está claro. Más adelante aparecen relaciones adicionales en función de las necesidades y el modelo se hace más complejo. Llega un momento en que el modelo se repite sobre sí mismo ABCA. A JPA no le gustan los bucles, por lo que es necesario codificar específicamente las partes que se gestionaron de forma automática. JPA ve en forma de árbol los objetos que se han de guardar; los bucles convierten el árbol en un grafo y en este preciso momento es cuando surgen los problemas.

Modelo en capas

  • Presentación

  • Negocio

  • DAO (Repository)

  • Base de datos

JPA 2 añade novedades que anteriormente solo estaban disponibles en Hibernate, como la gestión de colecciones de elementos con la anotación @ElementCollection, eliminación de huérfanos (orphan removal), uso de consultas tipadas, una caché de nivel 2 (L2), enlaces con la API Bean Validation (JSR-303) y la API Criteria.

Implementación

1. Configuración de una entidad de la capa de dominio

Por ejemplo, para configurar una entidad sencilla de la capa de dominio:

@Entity 
        @Table(name = "table_book"public class Libro implements Serializable {  
        @Id 
        @Column(name = "id") 
           private String autor; 
           private int numPaginas; 
           private String titulo; 
           // getters et setters 
          [...] 

Este ejemplo solo muestra las API y las anotaciones JPA porque es un caso sencillo:

Elemento

Significado

@Entity

Indica que se trata de una entidad POJO.

@Table(name = "t_book")

Especifica el nombre de la tabla public class Libro implements Serializable. Una entidad debe ser serializable.

@Id

Indica que el campo será una clave principal.

@Column(name = "id")

Especifica el nombre de la columna que contendrá la clave principal.

private String autor

Datos de negocio

El autor del libro

private int numPaginas

Datos de negocio

El número de páginas

private String titulo

Datos de negocio

El título

No hay ninguna anotación en los datos de negocio porque se trata de un tipo que Hibernate mapea sin dificultad a nivel del tipo y del nombre de la columna. De lo contrario, habríamos añadido una anotación @Column y personalizado la columna.

También es posible tener variables de clase que no están registradas en la base de datos. A continuación, la variable se anota con la anotación @Transient.

Se accede a las clases de la capa de dominio a través de la capa Repository.

Mapping many-to-one y one-to-many

@Entity 
        public class Book implements Serializable { 
            @Id 
            private String title; 
            private int nbPage;  
         
         @OneToMany 
         private Set<Chapter> chapters = 
         new HashSet<Chapter>(); 
             // getters et setters 

De forma predeterminada, las relaciones entre objetos son de tipo lazy loading. Esto significa que los datos contenidos en las colecciones solo se recuperan de la base de datos si se accede a ellos. Por lo tanto, es necesario evitar el uso de la palabra clave instanceOf sobre objetos Entity porque es normal que tengamos un proxy en lugar del objeto. Las Entities no deben ser del tipo «final» para que pueden ser modificadas por JPA o mediante programación orientada a aspectos (AOP).

Tipos de datos

Es posible crear sus propios tipos, pero esto está más allá del alcance de esta introducción.

Podemos usar la anotación @EntityListeners en la entidad para detectar cambios. Esto va en contra del modelo en capas porque añade código a la entidad que normalmente contiene solo código de validación.

2. Acceso al objeto de dominio

Acceso al objeto de dominio desde un objeto de capa Repository EntityManager

@ApplicationScoped 
        public class PersonRepository @PersistenceContext 
            private EntityManager em; 
            public Person createOrUpdateBook(Book book) { 
                return em.merge(book); 
            } 
            public List<Book> findAll() { 
                return em.createNamedQuery("findAllBooks").getResultList(); 
            } 
            public Person findBook(String titulo) { 
                return em.find(Book.class, titulo); 
            } 

La capa Repository contiene el EntityManager em, que es un espacio de memoria que contiene los datos que se están modificando. EntityManager administra la fusión entre los objetos de su caché y los objetos externos que desea integrar en la memoria caché para una actualización futura. También gestiona transacciones. Se puede considerar un súper-DAO porque es él quien facilita el acceso a la base de datos. También proporciona proxies en objetos vinculados (relación o colección) que están en lazy loading y devuelve objetos «reales» cuando se accede a entidades.

Le permite administrar directamente colecciones de tipos primitivos, como Strings, Integers, etc., a nivel de base de datos.

Ejemplo

@Entity 
        public class Index {  
        @ElementCollection 
            private Collection<String> words; 

La anotación @ElementCollection es una anotación equivalente a la anotación Hibernate @OneToMany.

3. Eliminación en cascada

Sin Hibernate, primero tenía que borrar los elementos hijo y, posteriormente, borrar el elemento principal (padre).

Secuencia:

1. Buscar el elemento que desea eliminar.

2. Encontrar los hijos de este elemento.

3. Borrar todos sus hijos.

4. Borrar el elemento principal.

Con Hibernate, es suficiente con especificar explícitamente el annotationCascadeType.DELETE_ORPHAN, que se utiliza junto con CascadeType.ALL JPA.

Con JPA 2.0, la opción orphanRemoval = true se utiliza en la anotación de la relación:

@OneToMany(mappedBy="foo", orphanRemoval=true) 

4. Consultas tipadas

Con anterioridad, era necesario hacer cast sistemáticamente del tipo de retorno de las consultas.

Query q1 = em.createQuery("SELECT c FROM Country c"); 
        List results = query.getResultList(); 

Ahora podemos usar la genericidad para especificar el tipo de esta manera:

TypedQuery<Country> q2 = 
              em.createQuery("SELECT c FROM Country c", Country.class); 
        List<Country> countries = query.getResultList(); 

5. Caché de nivel 1 y 2 Caché

La caché de nivel 1, utilizada por EntityManager, contiene las transacciones actuales. Al final de la transacción, se renueva la memoria caché. Para guardar en bases de datos, fusionamos las entidades y vaciamos la caché mediante una acción de flush. EntityManager almacena una copia del estado de la base de datos a la que se accedió por última vez. Hay varios modos en relación con el bloqueo de datos que se modifican.

En el modo de bloqueo optimista (optimistic-lock), se utiliza un campo anotado con @version para ayudar a JPA a verificar si los datos se han modificado (por otro EntityManager JPA) desde el último acceso. En caso de problemas o si algo sale mal, JPA lanza la excepción OptimisticLockException.

Esto solo funciona si la base de datos se modifica únicamente desde JPA. Si los triggers o procedimientos almacenados modifican los datos, entonces la memoria caché es falsa y se debe invalidar manualmente.

La nueva anotación @Cacheable habilita la caché de nivel 2. Esta caché es relativamente difícil de configurar. Usamos diferentes implementaciones si tenemos un servidor o un clúster. De hecho, en el caso de un clúster, se necesita una caché compartida entre los miembros del clúster con, si es posible, una subred dedicada para los flujos intercambiados.

Las cachés principales de nivel 2 son Ehcache, Hazelcast e Infinispan. El almacenamiento en caché se analiza en el capítulo Spring y NoSQL.

6. Bean Validation (JSR-303) Bean Validation

La validación Hibernate se utilizó antes de que esta API estuviera disponible. La Bean Validation no es específica de JPA. Algunas validaciones no son compatibles con JSR-303, pero están disponibles en la validación Hibernate. Por lo tanto, estos dos tipos de validación a menudo se utilizan de manera conjunta, siempre prefiriendo los de JPA en caso de redundancia.

Ejemplo

import java.util.Date; 
        import javax.validation.constraints.NotNull; 
        import javax.validation.constraints.Past; 
        import javax.validation.constraints.Size; 
        public class Coche { 
          private String marca; 
          private Date fechaMatr;  
          @NotNull 
          @Size(max=50) 
          public String getMarca() { 
            return marca; 
          } 
          public void setMarca(String marca) { 
            this.marca = marca; 
          }  
          @Past 
          public Date getFechaMatr() { 
            return fechaMatr; 
          } 
          public void setDateMatr(Date fechaMatr) { 
            this.dateMatr = fechaMatr; 
          } 

Comprobamos que los campos no sean nulos, que la marca no tenga más de 50 caracteres y que la fecha de matriculación está en el pasado.

Antes de hacer una copia de seguridad de los datos, los validamos y obtenemos una lista de errores. Si se intenta guardar sin validar, JPA genera una excepción ConstraintViolationException e invalida la transacción.

Para asegurarnos de que no tenemos datos incorrectos en la base de datos, debemos hacer coincidir las restricciones administradas por JPA/Hibernate con las restricciones contenidas en la base de datos. Esto es fundamental porque permite generar el esquema de la base de datos a partir del código Java y generar el código Java a partir de la base de datos con la ayuda de herramientas.

Puede llamar explícitamente a la validación antes de solicitar una copia de seguridad: 

ValidatorFactory factory = 
        Validation.buildDefaultValidatorFactory(); 
        Validator validator = factory.getValidator(); 
        Set<ConstraintViolation<Book>> constraintViolations =  
        validator.validate(book); 
        for (ConstraintViolation<Book> constraintViolation : 
           constraintViolations) { 
         log.debug(constraintViolation.getPropertyPath() + " - " + 
         constraintViolation.getMessage()); 

Esto permite no invalidar la transacción y recuperar errores para mostrarlos si es necesario.

Ejemplos de anotaciones estándares: Anotaciones

Anotación

Significado

@Size(min=2, max=240)

Controla la longitud del campo.

@AssertTrue / @AssertFalse

Comprueba una condición.

@Null / @NotNull

Comprueba si el valor es nulo.

@Max / @Min

Compara el valor respecto al min o al max.

@Future / @Past

Compara respecto a una fecha (u hora) determinada.

@Digits(integer=6, fraction=2)

Comprueba cuántos dígitos hay en el número.

@Pattern(regexp="\\ (\\d+\\)]?\\d+-\\d+")

Pruebas a través de una expresión regular (por ejemplo, para un número de teléfono: "(555)123-4567".

Anotaciones validador Hibernate:

Anotación

Significado

@Email

Correo electrónico

@URL

URL

@CreditCardNumber

Tarjeta de crédito

@Range

Franja o rango

7. La API Criteria Criteria

La API Criteria de JPA es un complemento de JPQL (el lenguaje de consulta JPA). Su ventaja es su fuerte tipado, que elimina muchos de los errores que se detectan durante la compilación, en lugar de en tiempo de ejecución. Se utiliza para consultas complejas y, en algunos casos, permite guiar JPA durante la construcción de la consulta SQL para la base de datos.

También permite crear consultas dinámicamente en Java.

EntityManager em = ...; 
        CriteriaBuilder cb = em.getCriteriaBuilder(); 
        CriteriaQuery<Cuenta> cq = cb.createQuery(Cuenta .class); 
        Root<Cuenta> cuenta = cq.from(Cuenta.class); 
        cq.select(cuenta); 
        TypedQuery<Cuenta> q = em.createQuery(cq); 
        List<Cuenta> all Cuenta s = q.getResultList(); 

El equivalente de la consulta JPQL es:

SELECTFrom Cuenta c 

8. Acceso a la base de datos

Accedemos a la base de datos a través de una conexión única o a través de un DataSource. A menudo, la gestión de las conexiones se delega a Spring, lo que permite disponer de conexiones de diferentes tipologías en función de los entornos. 

9. El archivo persistence.xml persistence.xml

El archivo persistence.xml es uno de los archivos principales de la configuración de JPA. Por lo general, está en src/main/resources/META-INF/. Se configura en Maven a través del «resource filtering».

Ejemplo de persistence.xml:

<?xml version="1.0" encoding="UTF-8"?> 
        <persistence version="2.0" 
                     xmlns=" http://java.sun.com/xml/ns/persistence " 
                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                     xsi:schemaLocation=" 
               http://java.sun.com/xml/ns/persistence 
               http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> 
             <persistence-unit name="primary"> 
                 <jta-data-source> 
        java:jboss/datasources/CompanyFight</jta-data-source> 
                 <properties> 
                     <property name="hibernate.hbm2ddl.auto" value="update" /> 
                     <property name="hibernate.dialect" 
                        value=" org.hibernate.dialect.PostgreSQLDialect"/> 
                     <property name="hibernate.show_sql" value="true"/> 
                     <property name="hibernate.generate_statistics" value="false"/> 
                 </properties> 
             </persistence-unit> 
        </persistence> 

Este archivo contiene la configuración de la base de datos para el mapping y proporciona la información de la conexión a la base de datos.

Especificamos en los tags <persistence-unit> los argumentos de una unidad de persistencia. Un tag tiene el atributo name, que indica el nombre de la unidad. También podríamos tener el atributo transaction-type, que especifica el tipo de transacción utilizada si no queremos usar la predeterminada.

10. Pruebas JPA Pruebas

Las pruebas se exploran más a fondo en el capítulo sobre las pruebas.

Las pruebas JPA son fáciles de codificar con Spring. Permiten anticipar una gran parte de los problemas que no se verían hasta demasiado tarde.

Estas pruebas mejoran el código, comprueban que se tienen en cuenta las reglas de negocio, ayudan a evitar regresiones y permiten una corrección rápida cuando se automatizan las builds.

La cobertura de las pruebas es más importante cuando se usa un generador de código como jHipster o Celerio porque es posible la generación sistemática de pruebas. Por lo general, usaremos jUnit o TestNG para codificar las pruebas. jHipster

JPA, stubs y mocks

Spring está basado en las interfaces. El framework encuentra para nosotros la mejor clase que implementa la interfaz del objeto sobre el que queremos trabajar.

Esto es particularmente práctico para las pruebas porque podemos construir objetos que implementen una interfaz mientras se vacían de su sustancia. Por ejemplo, podemos hacer un DAO que exponga los métodos, pero devuelva conjuntos de pruebas en lugar de acceder a una base de datos real. Este tipo de objeto se denomina «stub».

Esto es muy práctico, pero se puede mejorar. De hecho, es necesario crear el objeto respetando los contratos de una interfaz, incluso aunque no se utilicen todos sus métodos.

Una herramienta basada en una librería que generara objetos sobre la marcha, que respetara las interfaces y con la que programáramos solo las interfaces que se quiere probar sería más productiva. Esta herramienta existe y la llamamos generador de mocks.

Los mocks son más sencillos y rápidos de usar que los stubs. Las librerías Mockito (http://code.google.com/p/mockito/) y EasyMock (http://easymock.org/) se utilizan de manera general. Spring también proporciona un conjunto muy completo de mocks ya realizados.

Ejemplo de simulación de un EntityManager mediante un mock para realizar pruebas

@Test@Test 
        public void testCarPersist { 
           carRepository carRepository = 
             new carRepository(); 
           EntityManager em = EasyMock.createMock(EntityManager.class); 
           carRepository.setEntityManager(em) ; 
            Car car = new Car("Ford T"); 
            em.persist(car) ; 
         EasyMock.replay(em); 
            personRepository.save(car); 
         EasyMock.verify(em); 
         } 

Para probar la capa de dominio, usaremos el contenedor Spring. En caso de que hagamos Spring y JPA en un servidor Jakarta EE, usamos Arquillian.

Ejemplo de prueba de integración con Spring

@RunWith(SpringJUnit4ClassRunner.class) 
        @ContextConfiguration(locations= 
        {"classpath*:/META-INF/spring/application-context-test.xml"}) 
        public class IntegrationTest @Inject 
        private CarService carService; 
        @Test 
        public void createCar() { 
          try { 
            carService.findCar ("test_car"); 
            fail("Car already exists in the database."); 
          } catch (ObjectRetrievalFailureException orfe) { 
          // User should not already exist in the database. 
          } 
          Car car = new Car(); 
          car.setLogin("test_car"); 
          car.setFirstName("Name"); 
          carService.createCar(car); 
          Car carFoundInDatabase = carService.findCar("test_car"); 
          assertEquals("Name", carFoundInDatabase.getFirstName()); 
          } 

Para ir más allá

Existen frameworks que simplifican enormemente la implementación de un backend, que suele funcionar gracias a las anotaciones de procesadores.

1. Librería Java jcabi-aspects

Esta librería se basa en AspectJ y ofrece algunas anotaciones de AOP cubiertas por Spring o Lombok, como @Async, @Cachable, @Loggable, y otras que faltarían, como @LogExceptions, @Quietly, @RetryOnFailure, @UnitedThrow. La documentación se puede encontrar aquí: https://aspects.jcabi.com/

2. Métricas AspectJ

Hay una librería que permite añadir métricas en nuestros backends a través de anotaciones AspectJ. Permiten tener elementos finos en las llamadas. La documentación está disponible en https://github.com/astefanutti/metrics-aspectj

Usar MapStruct

Hemos visto que nuestro código se organiza en capas. Podemos utilizar el modelo DTO, que consiste en definir clases sencillas para transferir los datos entre capas. Uno de los principales problemas que encontramos es la escritura de una gran cantidad de código de mapeo. Existe una librería de Java MapStruct que permite automatizar, con la generación de código, la fase de mapping a través de la descripción de esta mediante interfaces sencillas y anotaciones sobre las clases. MapStruct se puede utilizar con el CDI de Spring junto con Lombok, creando el Mapper como un bean. La documentación se puede encontrar en https://mapstruct.org/documentation/spring-extensions/reference/html/

1. Enfoque API-First

El diseño de backend para los Web Services SOAP o REST ha existido durante años. Durante mucho tiempo, la API se dedujo de las llamadas codificadas en los backends. Codificamos un servicio y luego añadimos y publicamos la API correspondiente a ese servicio, algunas veces de manera automática. Esto da como resultado API que no siempre son ideales.

Un nuevo enfoque consiste en predefinir la API y, a continuación, codificar la aplicación para representar el servicio llamado.

No hay estándares predefinidos para la API REST. Surgió un primer estándar: Swagger, sustituido más tarde por OpenAPI. Esta normalización o estandarización, que hace que las API sean consistentes y reutilizables, ha causado que la codificación también se haya automatizado parcialmente.

La visión API-First también permite centrarse en un producto y provoca un desacoplamiento entre el productor y el consumidor de la API.

Hemos visto que REST (Representational State Transfer) puede seguir un estándar. De hecho, hay varias ramas:

  • Swagger 2.0

  • OpenAPI 3.0

Herramientas

Swagger y OpenAPI tienen una variedad de herramientas que son complementarias. Ambas líneas siguen los mismos estándares.

1. Swagger

Las fuentes están disponibles en GitHub, en la dirección https://github.com/swagger-api. Swagger admite varias herramientas:

Herramientas

Utilidad

Swagger Editor

Editor de API

Swagger UI

Visualización e interacciones con la API

Swagger Codegen

Generadores de código/archivos de API

2. OpenAPITools

Las fuentes están disponibles en GitHub, en la dirección https://github.com/OpenAPITools. OpenAPITools soporta varias herramientas:

Herramientas

Utilidad

OpenAPI Generator

Generadores de código/archivos de API

OpenAPI Style Validator

Cumplimiento de los estándares corporativos

OpenAPI Diff

Comparación de dos especificaciones

3. Otros

Hay otros que se enumeran y se pueden consultar en https://openapi.tools/

Generadores de código

Tanto Swagger como OpenAPI cuentan con una suite de herramientas que permiten, entre otras cosas, generar código a partir de una descripción de API y viceversa. Ya hemos visto el plugin Springfox para generar la descripción de la API, desde el código hasta la compilación, durante el runtime. También tienen herramientas para generar código a partir de la descripción de la API.

La descripción de la API se realiza a través de un archivo JSON o YAML. A partir de este archivo, es posible generar código en diferentes lenguajes y para diferentes servidores. Nos centramos en generar código cliente y servidor con las tecnologías Java y Spring Boot. El generador es polimórfico. Existe en forma de CLI (Command Line Interface) en un JAR, de paquete npm y de plugin maven.

La versión maven es la más sencilla para poder hacer modificaciones. En efecto, tenemos el mismo «problema» que con los generadores de código como jHipster, para los cuales la plantilla que se utiliza para la generación no se corresponde exactamente con los archivos que serían deseables. Los generadores Swagger y OpenAPI no incluyen los mismos conjuntos de templates y cada uno tiene sus características específicas.

Uso del plugin

1. Para el generador Swagger

Configuración maven

<plugin> 
        <groupId>io.swagger</groupId> 
        <artifactId>swagger-codegen-maven-plugin</artifactId> 
        <version>3.8.1</version> 
        <executions> 
         <execution> 
          <goals> 
           <goal>generate</goal> 
          </goals> 
          <configuration> 
           <inputSpec>swagger.yaml</inputSpec> 
           <language>java</language> 
           <library>resttemplate</library> 
          </configuration> 
         </execution> 
        </executions> 
        </plugin> 

2. Para el generador OpenAPITools

Configuración maven

<plugin> 
        <groupId>org.openapitools</groupId> 
        <artifactId>openapi-generator-maven-plugin</artifactId> 
        <version>5.4.0</version> 
        <executions> 
         <execution> 
          <goals> 
        <goal>generate</goal> 
          </goals> 
          <configuration> 
        <inputSpec> 
         ${project.basedir}/src/main/resources/petstore.yml 
        </inputSpec> 
        <generatorName>spring</generatorName> 
        <apiPackage>fr.eni.openapi.api</apiPackage> 
        <modelPackage>fr.eni.openapi.model</modelPackage> 
        <supportingFilesToGenerate> 
         ApiUtil.java 
        </supportingFilesToGenerate> 
        <configOptions> 
         <delegatePattern>true</delegatePattern> 
        </configOptions> 
          </configuration> 
         </execution> 
        </executions> 
        </plugin> 

También existe una versión beta 6.0.0.

Personalización

Es posible personalizar los templates que se utilizan para la generación de código. 

1. Swagger

La personalización del generador Swagger es un poco más compleja. La forma más sencilla consiste en parchear un conjunto de plantillas. El motor de plantillas es Mustache.

2. OpenAPITools

La personalización de OpenAPI es relativamente sencilla. Hay varios niveles de personalización, como se indica en el sitio del producto, en la dirección https://openapi-generator.tech/docs/customization/. Funciona bien y el motor de plantillas predeterminado es Mustache.

Diseño de una descripción de API

Si diseña una API que sigue una versión del estándar OpenAPI, no significa necesariamente que será utilizable porque los generadores de código no siempre implementan código que cubra todo el estándar. Se debe probar con las herramientas Swagger y OpenAPI a nivel de la generación de código de los DTO y de los controladores y clientes REST. Se deben probar las diferentes combinaciones Spring MVC/Spring WebFlux y Java/Kotlin en las diferentes versiones de estas. Las API que usan polimorfismo y herencia se deberán probar específicamente.

Si es posible, será necesario indicar las configuraciones probadas con el código generado con su forma de probarlo porque muchos problemas solo se ven en tiempo de ejecución.

Herramientas para el diseño de la API

La herramienta jHipster se puede utilizar para un enfoque API-First (https://www.jhipster.tech/doing-api-first-development/), pero ya debe tener su archivo YAML (o JSON).

No hay muchas herramientas públicas avanzadas que permitan crear un contrato de interfaz de API a partir de un modelo UML o de proyección en el sentido DDD del término.

Herramienta

Sitio

Código abierto

Stoplight

https://stoplight.io/studio

SwaggerHub

https://swagger.io/tools/swaggerhub/

Podemos configurar un modelador UML con generador de código Java para generar clases, anotarlas con anotaciones Swagger API y generar YAML (o JSON) a partir de estas clases personalizando las plantillas.

Spring Actuator

Spring Boot Actuator es un módulo que permite obtener información operativa sobre nuestra aplicación.

Para usarlo con Spring Boot, es suficiente con añadir la dependencia maven:

<dependency> 
           <groupId>org.springframework.boot</groupId> 
           <artifactId>spring-boot-starter-actuator</artifactId> 
        </dependency> 

El módulo funciona con las aplicaciones Spring MVC y Spring Webflux y utiliza los endpoints HTTP y JMX para exponer información.

Con Spring Boot 2, solo se exponen los endpoints /health e /info para limitar los problemas de seguridad.

La configuración de la seguridad se puede realizar a través de un bean de configuración:

@Bean 
        public SecurityWebFilterChain securityWebFilterChain( 
         ServerHttpSecurity http) { 
           return http.authorizeExchange() 
             .pathMatchers("/actuator/**").permitAll() 
             .anyExchange().authenticated() 
             .and().build(); 
        } 

Endpoint

Uso

/auditevents

Enumera los eventos relacionados con la auditoría de seguridad, como la conexión/desconexión. Podemos aplicar filtros.

/beans

Enumera todos los beans disponibles en nuestra BeanFactory. A diferencia de /auditevents, no admite el filtrado.

/condiciones

Crea un informe de condición sobre la configuración automática.

/configprops

Enumera todos los beans @ConfigurationProperties.

/env

Enumera las propiedades del entorno actual.

/flyway

Detalla nuestras migraciones de bases de datos Flyway.

/health

Resume el estado de salud de nuestra aplicación.

/heapdump

Construye y devuelve un heapdump de la JVM.

/info

Devuelve información general.

/liquibase

Como /flyway, pero para Liquibase.

/logfile

Devuelve los logs de aplicación ordinarios.

/loggers

Lee/modifica el nivel de log de nuestra aplicación.

/metrics

Detalla métricas genéricas y personalizadas para la aplicación.

/prometheus

Devuelve métricas como la anterior, pero formateadas para funcionar con un servidor Prometheus.

/scheduledtasks

Proporciona detalles sobre cada tarea planificada en la aplicación.

/sessions

Enumera las sesiones HTTP, ya que usamos Spring Session.

/shutdown

Realiza una parada progresiva de la aplicación.

/threaddump

Vacía la información de thread de la JVM subyacente.

El endpoint /actuator devuelve enlaces a los diferentes endpoints en formato HATEOAS.

Puntos clave

  • Los objetos de la capa de negocio son objetos de tipo Entity gestionados por una EntityManager.

  • Algunas veces, los objetos están disponibles como proxies.

  • JPA ofrece el uso de múltiples niveles de caché.

  • Podemos tener tipos personalizados.

  • La gestión de concurrentes es ajustable.

  • Mejoraremos la mantenibilidad de la aplicación, respetando el principio de las capas de aplicación.

Spring MVC JSP

Spring MVC permite crear fácilmente una aplicación web en un contenedor ligero como Tomcat o Jetty. Permite servir una interfaz hombre-máquina (IHM) con JSP o JSF u otro motor de renderizado como Thymeleaf y exponer API SOAP y REST. Es muy fácil de utilizar y permite tener una muy buena cobertura de pruebas. Es posible usar una configuración basada en archivos XML o anotaciones. Al igual que sucede con otros tipos de aplicaciones Spring, son preferibles las configuraciones anotadas porque son más fáciles de mantener. En este capítulo, presentamos los dos tipos de configuración porque los proyectos más antiguos todavía usan la versión basada en archivos XML. Los servicios web SOAP y REST algunas veces usan Spring MVC, junto con Jersey y Apache CXF.

Este capítulo se centra en Spring MVC sin Spring Boot, que será el tema de un capítulo específico (Spring Boot). Para los nuevos proyectos, normalmente ya no se utilizan las JSP. En teoría, las páginas JSP se han sustituido por páginas JSF, pero en la práctica las SPA (Single Page Applications) han tenido prioridad sobre las JSP (y JSF). Sin embargo, todavía se utilizan las partes inicialmente previstas para un uso JSF con el RestTemplate, lo que permite el uso simplificado de Spring MVC para su utilización con servidores REST. Sin embargo, sigue siendo interesante conocer esta parte para poder hacer mantenimiento y pasar ciertas pruebas técnicas durante la selección.

Preferiblemente, usaremos el WebClient del stack reactivo para los nuevos proyectos que hagan Spring MVC (bloqueadores) porque RestTemplate ya no evolucionará a favor de evoluciones en el WebClient. De hecho, el WebClient es un cliente de bloqueo normal y un cliente reactivo. El WebClient se detalla en el capítulo titulado Introducción a Spring Reactor y Spring Webflux.

Al igual que sucede con muchos módulos de Spring, encontramos en los proyectos de configuraciones mezcladas varios estilos correspondientes a varios períodos de programación. Este capítulo muestra maneras antiguas y nuevas de usar Spring MVC. Cuanto más moderna es la configuración, más sencillo parece. Por ejemplo, todavía es posible configurar una aplicación Spring MVC de manera muy fina usando programación, pero entender el origen de los argumentos de configuración en formato XML permite navegar más fácilmente a través de una comprensión transversal.

La configuración se debe documentar con comentarios en los archivos. De hecho, es más fácil hacerlo siempre y cuando el tema sea nuevo. La configuración concentra muchas dificultades durante sus evoluciones. También veremos en un capítulo específico que es posible hacer aplicaciones reactivas Spring WebFlux y que, en este caso, gran parte de la complejidad de la configuración está oculta, pero permanece accesible. Como de costumbre, la mejor configuración sigue siendo la más sencilla con respecto a la necesidad que se debe cubrir.

Los ejemplos de este capítulo son muy detallados. No se ha podido transferir todo el código a estas páginas. Lo ideal es tener en su IDE los ejemplos del libro para ver cómo se utilizan los elementos y, si es necesario, poder hacer algunas pruebas durante la lectura para retener los muchísimos elementos que se presentan. Los nombres de los elementos relativos a los ejemplos se suelen indicar en el capítulo correspondiente.

Esta sección muestra el uso de la parte de servidor, con un foco sobre el uso de servlets controlados por Spring en un contexto MVC, con la parte front en JSP. En las aplicaciones más modernas usamos JSF, y en aplicaciones muy modernas solo usamos las API REST o GraphQL con la parte front hecha con frameworks como Angular, como veremos en el capítulo sobre el uso avanzado de este framework.

Sin embargo, durante nuestras misiones, será normal que tengamos que trabajar con aplicaciones antiguas. Históricamente, disponíamos de las aplicaciones Struts 1, luego Struts 2 o Spring MVC, que era la solución competidora.

1. Funcionamiento global

a. Configuración XML sencilla XML

Vamos a comenzar con el caso más sencillo, a saber, con una configuración XML. Para esta configuración, utilizamos una aplicación web clásica como punto de partida. Posteriormente, configuraremos el archivo web.xml para que la aplicación use Spring MVC. web.xml

Extracto del archivo web.xml

<servlet> 
        <servlet-name>hello-dispatcher</servlet-name> 
        <servlet-class>  
          org.springframework.web.servlet.DispatcherServlet 
        </servlet-class> 
        <init-param> 
        <param-name>contextConfigLocation</param-name> 
        <param-value>/WEB-INF/spring-mvc-config.xml</param-value> 
        </init-param> 
        <load-on-startup>1</load-on-startup> 
        </servlet> 
        <servlet-mapping> 
        <servlet-name>hello-dispatcher</servlet-name> 
        <URL-pattern>/</URL-pattern> 
        </servlet-mapping> 
        <listener> 
        <listener-class>  
          org.springframework.web.context.ContextLoaderListener 
        </listener-class> 
        </listener> 
        <context-param>  
        <param-name>contextConfigLocation</param-name> 
        <param-value>/WEB-INF/spring-core-config.xml</param-value> 
        </context-param> 

Usamos la clase org.springframework.web.servlet.DispatcherServlet para el servlet, especificando la ubicación del archivo de configuración en el argumento contextConfigLocation:

contextConfigLocation=/WEB-INF/spring-mvc-config.xml 

Y pedimos que el servlet se cargue durante el inicio:

load-on-startup=1 

Configuramos el listener ContextLoaderListener para que cargue el archivo de configuración de la parte back:

<listener> 
        <listener-class> 
          org.springframework.web.context.ContextLoaderListener 
        </listener-class> 
        </listener> 

y especificamos la ubicación del archivo de configuración back en el contextConfigLocation.

Luego creamos la configuración en el archivo spring-mvc-config.xml:

<context:component-scan base-package="fr.eni.spring.mvc.xml.web"/> 
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> 
        <property name="prefix" value="/WEB-INF/views/jsp/" /> 
        <property name="suffix" value=".jsp" /> 
        </bean>  
        <mvc:resources mapping="/resources/**" location="/resources/" />  
        <mvc:annotation-driven /> 

Indicamos dónde están los web beans anotados en el context:component-scan.

Creamos el bean InternalResourceViewResolver para el resolver de vistas predeterminadas, con las siguientes propiedades:

  • viewClass=org.springframework.web.servlet.view.JstlView"

  • prefix="/WEB-INF/views/jsp/"

  • suffix=". jsp"

A continuación, indicamos dónde están los archivos de recursos:

<mvc:resources mapping="/resources/**" location="/resources/" /> 

e indicamos que los beans Spring MVC usan anotaciones:

<mvc:annotation-driven /> 

A continuación, podemos crear el controlador con una clase anotada @Controller:

@Controller 
        public class HolaController { 
          private final Logger logger = 
        LoggerFactory.getLogger(HolaController.class); 
          private final HolaService holaService;  
          //@Autowired : como recordatorio: opcional en Spring 4+ 
          public HolaController(HolaService holaService) { 
           this.holaService = holaService; 
          } 
          @RequestMapping(value = "/", method = RequestMethod.GET) 
          public String index(Map<String, Object> model) { 
           model.put("titulo", holaService.getTitulo("")); 
        model.put("mensaje", holaService.getMensaje()); 
           return "index"; 
          } 
          @RequestMapping(value = "/hola/{nombre:.+}", method = 
        RequestMethod.GET) 
          public ModelAndView hola(@PathVariable("nombre") String name) { 
           ModelAndView model = new ModelAndView(); 
           model.setViewName("index"); 
           model.addObject("título", holaService.getTitulo(name)); 
           model.addObject("mensaje", holaService. getMensaje()); 
        return model; 
        } 

El controlador utiliza un servicio que se inyecta.

A través de la anotación @RequestMapping, indicamos el enrutamiento de los métodos en función de la URL:

  • Método al que se llama si no hay ningún argumento.

@RequestMapping(value = "/", method = RequestMethod.GET) 

El método que devuelve una página: index.

  • Método al que se llama si hay una acción hola con el argumento nombre.

@RequestMapping(value = "/hola/{nom:.+}", method = 
        RequestMethod.GET) 

El método devuelve un ModelAndView.

A continuación, creamos el servicio HolaService al que llama el controlador:

@Service 
        public class HolaService { 
          public String getMensaje() { 
           return "Ejemplo de aplicación Spring MVC versión XML"; 
          } 
          public String getTitulo(String nombre) { 
           logger.debug("getTitulo() is executed! $nombre: {}", nombre); 
           if(StringUtils.isEmpty(nombre)){ 
             return "Hola a todos "; 
           }else{ 
             return "Hola " + nombre; 
           } 
          } 

Este es un servicio estándar.

b. Configuración por anotaciones Anotaciones

La versión con anotación permite no tener un archivo web.xml.

Usamos una clase para la parte back y otra para la parte web.

Para la parte back:

@Configuration 
        @ComponentScan({ "fr.eni.mvc.annot.service" }) 
        public class SpringBackConfig { 

Para la parte web:

@EnableWebMvc 
        @Configuration 
        @ComponentScan({ "fr.eni.mvc.annot.web" }) 
        public class SpringWebConfig extends WebMvcConfigurerAdapter { 
          @Override 
          public void addResourceHandlers(ResourceHandlerRegistry registry) { 
           registry.addResourceHandler("/resources/**") 
               .addResourceLocations("/resources/"); 
          } 
         
          @Bean 
          public InternalResourceViewResolver viewResolver() { 
           InternalResourceViewResolver viewResolver 
             = new InternalResourceViewResolver(); 
           viewResolver.setViewClass(JstlView.class); 
           viewResolver.setPrefix("/WEB-INF/views/jsp/"); 
           viewResolver.setSuffix(".jsp"); 
           return viewResolver; 
        } 

La parte web usa las siguientes anotaciones para la configuración:

  • @EnableWebMvc: indica que se trata de una configuración Spring MVC.

  • @Configuration: bean de configuración.

  • @ComponentScan({ "fr.eni.mvc.annot.web" }): los paquetes que se van a analizar en busca de las clases anotadas.

El resto de las clases Service y Controller son idénticas a las de la configuración XML.

2. Elementos complejos del controlador Controlador

La clase controlador anotada con @Controllergestiona el enrutamiento de las consultas.

a. Funcionamiento global del controlador

El controlador interpreta los datos que provienen del cliente y los transforma en un modelo correspondiente a una vista para el usuario.

Utilizamos la anotación @Controller para identificar los controladores.

El Request Mapping se puede hacer desde los métodos o desde la clase para un primer nivel, seguido por el método para un segundo nivel.

Un primer nivel en la clase y un segundo en el método:

@Controller  
        @RequestMapping("/class-mapping/*") //Annotation Clase 
        public class ClasslevelMappingController {  
          @RequestMapping("/pathc") 
        public @ResponseBody String porElPath() { 
           return "Mapeado por el path"; 
          } 

es equivalente a un nivel únicamente en el método:

@Controller 
        public class PorMappingController {  
          @RequestMapping("/mapping/pathm") 
          public @ResponseBody String porElPath() { //Annotation Método 
           return "Mapeado por el path"; 
          } 

El controlador más simple indica para una URL /mapping/pathm que el método porElPath() realiza el procesamiento y devuelve la página.

Para abreviar, en los ejemplos nos basamos en un enrutamiento solo en el método utilizado por los controladores, pero el principio sigue siendo el mismo si compartimos el enrutamiento entre la clase y el método.

La prueba unitaria por path es un poco más compleja porque tiene que crear un mock MockMvc e inicializarlo:

@RunWith(SpringJUnit4ClassRunner.class) 
        public class ParMappingControllerTests extends 
        AbstractContextControllerTests { 
          private MockMvc mockMvc; 
          @Before 
          public void setup() throws Exception { 
           this.mockMvc = 
           webAppContextSetup(this.wac).alwaysExpect(status().isOk()) 
                         .build(); 
        @Test 
        public void byPathc() throws Exception { 
           this.mockMvc.perform(get("/mapping/pathc")).andExpect(content() 
                      .string("Mapeado por el path")); 

Reutilizamos el mock mockMvc para el resto de las pruebas del Request Mapping.

b. Ejemplo de clase simple

Comenzaremos con el caso más simple. Se trata de mostrar una página que responde a una consulta GET sobre la URL /primera:

Código en la página:

Añadimos un href para apuntar al servlet:

<a id="simpleLink" class="textLink" href="<c:url value="/primera" 
        />">GET /primera</a> 

En el controlador:

Este controlador es sencillo:

@RequestMapping("/primera") 
        public @ResponseBody String simple() { 
        logger.debug("Respuesta"); 
          return "ENI os dice hola"; 
        } 

Pruebas unitarias:

La TU carga la página, comprueba el estado y el tipo del contenido.

@Test 
        public void simple() throws Exception { 
          standaloneSetup(new PrimerControlador()).build()  
           .perform(get("/primera")) 
           .andExpect(status().isOk()) 
           .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 
           .andExpect(content().string("ENI os dice hola")); 

c. Revisión sencilla

El método simple2() especifica que solo acepta consultas de tipo GET.

Código en la página:

<a id="simpleRevisited" class="textLink" href="<c:url 
        value="/primera/revisada" />">GET /primera/revisada</a> 

En el controlador:

@RequestMapping(value="/primera/revisada", 
        method=RequestMethod.GET, headers="Accept=text/plain"public @ResponseBody String simple2() { 
          return "ENI les desea buen provecho"; 

Pruebas unitarias:

@Test  
        public void simple2() throws Exception { 
          standaloneSetup(new PrimerControlador()).build() 
           .perform(get("/primera/revisada").accept(MediaType.TEXT_PLAIN)) 
           .andExpect(status().isOk()) 
           .andExpect(content().contentType("text/plain")) 
           .andExpect(content().string("ENI les desea buen provecho")); 

d. Por el path Path

El mapping se realiza en relación con el path.

Código en la página:

<a id="byPath" class="textLink" href="<c:url 
        value="/mapping/path" />">Por el path</a> 

En el controlador:

@RequestMapping("/mapping/path"public @ResponseBody String byPath() { 
          return "Mapeo por path!"; 

Pruebas unitarias:

@Test 
        public void byPath() throws Exception { 
          this.mockMvc.perform(get("/mapping/path")) 
                .andExpect(content() 
        .string("Mapeo por path")); 

En el controlador:

Con más opciones:

@RequestMapping(value="/mapping/path/*", 
        method=RequestMethod.GET) 
        public @ResponseBody String byPathPattern(HttpServletRequest request) { 
          return "Mapeado por el path pattern ('" + request.getRequestURI() + "')"; 
        } 

e. Por un patrón en el path

La anotación @RequestMapping también procesa rutas de tipo Ant (por ejemplo, /myPath/*.do).

Los patrones URI se pueden mezclar con variables.

Código en la página:

<a id="byPathPattern" class="textLink" href="<c:url 
        value="/mapping/path/wildcard" />">Por un patrón sobre el path</a> 

Pruebas unitarias:

@Test 
        public void byPathPattern() throws Exception { 
          this.mockMvc.perform(get("/mapping/path/wildcard")) 
             .andExpect(content().string("Mapeo por el path pattern 
        ('/mapping/path/wildcard')")); 

f. Por el path y un método

Podemos utilizar la URL para apuntar a un método a través de un path con un nombre de método.

Código en la página:

<a id="byMethod" class="textLink" href="<c:url 
        value="/mapping/method" />">Por el path y un método</a> 

En el controlador:

@RequestMapping(value="/mapping/method", 
        method=RequestMethod.GET) 
        public @ResponseBody String byMethod() { 
          return "Mapeo por path + método"; 

Pruebas unitarias:

@Test 
        public void byMethod() throws Exception { 
          this.mockMvc.perform(get("/mapping/method")) 
        .andExpect(content().string("Mapeado por path + método")); 

g. Mapeado por path + método + presencia de argumentos de query

Podemos pasar argumentos en la URL.

Código en la página:

<a id="byParameter" class="textLink" href="<c:url 
        value="/mapping/parameter?foo=bar" />">Mapeado por path + método + 
        presencia de argumentos de query</a> 

En el controlador:

@RequestMapping(value="/mapping/parameter", 
        method=RequestMethod.GET, params="foo") 
        public @ResponseBody String byParameter() { 
          return "Mapeado por path + método + presencia de argumentos de 
        query";  

Pruebas unitarias:

@Test 
        public void byParameter() throws Exception { 
          this.mockMvc.perform(get("/mapping/parameter?foo=bar")) 
        .andExpect(content().string("Mapeado por path + método + 
        presencia de argumentos de query")); 

h. Mapeado por path + método + presencia de un header

Podemos pasar argumentos en la URL y especificar el método al que hay que llamar:

Código en la página:

<a id="byHeader" href="<c:url value="/mapping/header" />">Por la 
        presencia de un header</a> 

En el controlador:

@RequestMapping(value="/mapping/header", 
        method=RequestMethod.GET, headers="FooHeader=foo"public @ResponseBody String byHeader() { 
          return "Mapeado por path + método + presencia de un header!"; 

Pruebas unitarias:

@Test 
        public void byHeader() throws Exception { 
          this.mockMvc.perform(get("/mapping/header").header("FooHeader""foo")) 
        .andExpect(content().string("Mapeado por path + método + presencia 
        de un header")); 

i. Por la ausencia de un header

Podemos llamar a un método si detectamos la ausencia del header:

Código en la página:

<a id="byHeaderNegation" class="textLink" href="<c:url 
        value="/mapping/header" />">Por la ausencia de un header</a> 

En el controlador:

@RequestMapping(value="/mapping/header", 
        method=RequestMethod.GET, headers="!FooHeader"public @ResponseBody String byHeaderNegation() { 
          return "Mapeado por path + método + ausencia de un header"; 

Pruebas unitarias:

@Test 
        public void byHeaderNegation() throws Exception { 
           this.mockMvc.perform(get("/mapping/header")) 
        .andExpect(content().string("Mapeado por path + método + ausencia 
        de un header")); 

j. Por consumo

Podemos tener un método de controlador que consume un recurso, por ejemplo, un JSON:

Código en la página:

<form id="byConsumes" class="readJsonForm" action=" 
        <c:url value="/mapping/consumes" />" method="post"> 
        <input id="byConsumesSubmit" type="submit" value="Por consumo" /> 
        </form> 

En el controlador:

@RequestMapping(value="/mapping/consumes", 
        method=RequestMethod.POST, 
        consumes=MediaType.APPLICATION_JSON_VALUE) 
        public @ResponseBody String byConsumes(@RequestBody JavaBean 
        javaBean) { 
           return "Mapeo por path + método + consumible por un tipo de 
        recurso multimedia (javaBean '" + javaBean + "')"; 

Pruebas unitarias:

@Test 
        public void byConsumes() throws Exception { 
           this.mockMvc.perform( 
                 post("/mapping/consumes") 
                    .contentType(MediaType.APPLICATION_JSON) 
                    .content("{ \"foo\": \"bar\", \"fruit\": \"apple\" }". 
        getBytes())) 
        .andExpect(content().string(startsWith("Mapeo por path + método  
        + consumible por un tipo de recurso multimedia (javaBean"))); 

k. Por producción a través de Accept=application/json

Podemos producir un recurso de tipo JSON en un servicio REST JSON clásico: JSON

Código en la página:

<a id="byProducesAcceptJson" class="writeJsonLink" href="<c:url 
        value="/mapping/produces" />">Por producción a través de 
        Accept=application/json</a> 

En el controlador:

@RequestMapping(value="/mapping/produces", 
        method=RequestMethod.GET,  
        produces=MediaType.APPLICATION_JSON_VALUE) 
        public @ResponseBody JavaBean byProducesJson() { 
           return new JavaBean(); 

Pruebas unitarias:

@Test 
        public void byProducesAcceptJson() throws Exception { 
           this.mockMvc.perform(get("/mapping/produces").accept  
        (MediaType.APPLICATION_JSON)) 
                 .andExpect(jsonPath("$.foo").value("bar")) 
                 .andExpect(jsonPath("$.fruit").value("apple")); 

l. Por producción a través de Accept=application/xml

Podemos pasar argumentos en la URL en formato XML o JSON: XML

En la página para XML:

<a id="byProducesAcceptXml" class="writeXmlLink" href="<c:url 
        value="/mapping/produces" />">Por producción a través de 
        Accept=application/xml</a> 

En el controlador:

@RequestMapping(value="/mapping/produces", 
        method=RequestMethod.GET, 
        produces=MediaType.APPLICATION_XML_VALUEpublic @ResponseBody JavaBean byProducesXml() { 
           return new JavaBean(); 

Pruebas unitarias:

@Test 
        public void byProducesAcceptXml() throws Exception { 
           this.mockMvc.perform(get("/mapping/produces") 
        .accept(MediaType.APPLICATION_XML)) 
                 .andExpect(xpath("/javaBean/foo").string("bar")) 
                 .andExpect(xpath("/javaBean/fruit").string("apple")); 

Código en la página para JSON:

JSON será la forma de hacer esto para los servicios REST.

<a id="byProducesJsonExt" class="writeJsonLink" href="<c:url 
        value="/mapping/produces.json" />">By produces via ".json"</a> 

Pruebas unitarias:

@Test 
        public void byProducesJsonExtension() throws Exception { 
           this.mockMvc.perform(get("/mapping/produces.json")) 
                 .andExpect(jsonPath("$.foo").value("bar")) 
                 .andExpect(jsonPath("$.fruit").value("apple")); 

Código en la página XML (variante):

<a id="byProducesXmlExt" class="writeXmlLink" href="<c:url 
        value="/mapping/produces.xml" />">By produces via ".xml"</a> 

Pruebas unitarias:

@Test 
        public void byProducesXmlExtension() throws Exception { 
           this.mockMvc.perform(get("/mapping/produces.xml")) 
                 .andExpect(xpath("/javaBean/foo").string("bar")) 
                 .andExpect(xpath("/javaBean/fruit").string("apple"));+ 

m. Argumentos de query

Podemos utilizar argumentos de consulta aislados:

Código en la página:

<a id="param" class="textLink" href="<c:url 
        value="/data/param?foo=bar" />">Argumentos de query</a> 

En el controlador:

@RequestMapping(value="param", method=RequestMethod.GET) 
        public @ResponseBody String withParam(@RequestParam String foo)return "El valor del argumento de query 'foo' [" + foo + "]"; 

Pruebas unitarias:

@Test 
        public void param() throws Exception { 
           this.mockMvc.perform(get("/data/param?foo=bar")) 
        .andExpect(content().string("El valor del argumento de query 
        'foo' [bar]"));  

n. Grupos de argumentos de consulta

Podemos utilizar múltiples argumentos de consulta:

Código en la página:

<a id="group" class="textLink" href="<c:URL 
        value="/data/group?param1=foo&param2=bar&param3=baz"/>">Grupo 
        de argumentos de query</a> 

En el controlador:

@RequestMapping(value="group", method=RequestMethod.GET) 
        public @ResponseBody String withParamGroup(JavaBean bean) { 
        return "El grupo de argumentos " + bean; 

Pruebas unitarias:

@Test 
        public void group() throws Exception { 
           this.mockMvc.perform(get("/data/group?param1= 
        foo&param2=bar&param3=baz")) 
        .andExpect(content().string(startsWith( 
                       "El grupo de argumentos 
        com.eni.spring.mvc.exemple.data.JavaBean@"))); 

o. Variable del path

Podemos usar el path para especificar una variable y su valor:

Código en la página:

<a id="var" class="textLink" href="<c:url value="/data/path/foo" 
        />">Variable del path</a> 

En el controlador:

@RequestMapping(value="path/{var}", method=RequestMethod.GET) 
        public @ResponseBody String withPathVariable(@PathVariable String 
        var) { 
        return "El valor de la variable 'var' del path [" + var + "]"; 

Pruebas unitarias:

@Test 
        public void pathVar() throws Exception { 
           this.mockMvc.perform(get("/data/path/foo")) 
        .andExpect(content().string("El valor de la variable 'var' del 
        path [foo]")); 

p. Cuerpo de consulta Consulta

Podemos usar el cuerpo de la request para el mapping:

Código en la página:

<form id="requestBody" class="textForm" action="<c:url 
        value="/data/body" />" method="post"> 
        <input id="requestBodySubmit" type="submit" value="Cuerpo de la 
        consulta" /> 
        </form> 

En el controlador:

@RequestMapping(value="body", method=RequestMethod.POST) 
        public @ResponseBody String withBody(@RequestBody String body) { 
        return "Cuerpo de la consulta enviada: [" + body + "]"; 

Pruebas unitarias:

@Test 
        public void requestBody() throws Exception { 
           this.mockMvc.perform( 
                 post("/data/body")  
                    .contentType(MediaType.TEXT_PLAIN) 
                    .content("foo".getBytes())) 
        .andExpect(content().string("Cuerpo de la consulta enviada: 
        [foo]")); 

q. Encabezado y cuerpo de la consulta

También podemos usar la cabecera y el cuerpo de la request.

Los datos transmitidos se pasan en el cuerpo.

Código en la página:

<form id="requestBodyAndHeaders" class="textForm" action="<c:url 
        value="/data/entity" />" method="post"> 
        <input id="requestBodyAndHeadersSubmit" type="submit" 
        value="Cabecera y cuerpo de la consulta" /> 
        </form> 

En el controlador:

@RequestMapping(value="entity", method=RequestMethod.POST) 
        public @ResponseBody String withEntity(HttpEntity<String> entity) { 
          return " Cuerpo de la consulta enviada [" + entity.getBody() + "]; 
          headers = " + entity.getHeaders(); 
        } 

Pruebas unitarias:

@Test 
        public void requestBodyAndHeaders() throws Exception { 
           this.mockMvc.perform( 
                 post("/data/entity") 
                    .contentType(MediaType.TEXT_PLAIN) 
                    .content("foo".getBytes())) 
                 .andExpect(content().string( 
                       "Cuerpo de la consulta enviada [foo]; headers = 
        {Content-Type=[text/plain], Content-Length=[3]}")); 
        } 

r. Argumentos en la consulta

Se pueden usar argumentos para la request.

Código en la página:

<a id="request" class="textLink" href="<c:url 
        value="/data/standard/request" />">Los argumentos de la consulta</a> 

En el controlador:

// request related 
        @RequestMapping(value="/data/standard/request", 
        method=RequestMethod.GET) 
        public @ResponseBody String 
        standardRequestArgs(HttpServletRequest request, Principal user,  
        Locale locale) { 
           StringBuilder buffer = new StringBuilder();  
           buffer.append("request = ").append(request).append(", "); 
           buffer.append("userPrincipal = ").append(user).append(", "); 
           buffer.append("requestLocale = ").append(locale); 
           return buffer.toString(); 

Pruebas unitarias:

@Test 
           public void request() throws Exception { 
              this.mockMvc.perform(get("/data/standard/request")) 
                    .andExpect(content().string(startsWith( 
                          "request = 
        org.springframework.mock.web.MockHttpServletRequest@"))); 
        } 

s. Argumentos de la respuesta

Podemos trabajar con los argumentos de la respuesta.

Código en la página:

<a id="response" class="textLink" href="<c:url 
        value="/data/standard/response" />">Los argumentos de la 
        respuesta</a> 

En el controlador:

// response related 
        @RequestMapping("/data/standard/respuesta"public @ResponseBody String response(HttpServletResponse respuesta) { 
           return "respuesta = " + respuesta; 

Pruebas unitarias:

@Test 
        public void respuesta() throws Exception { 
           this.mockMvc.perform(get("/data/standard/respuesta"))  
                 .andExpect(content().string(startsWith( 
                       " respuesta = 
        org.springframework.mock.web.MockHttpServletResponse@"))); 

t. Sesión Sesión

Es posible recuperar los elementos de la sesión como el SESSIONID con un controlador de tipo @RequestMapping("/data/standard/session"):

Código en la página:

<a id="session" class="textLink" href="<c:URL 
        value="/data/standard/session" />">Sesión</a> 

En el controlador:

// HttpSession 
        @RequestMapping("/data/standard/session"public @ResponseBody String session(HttpSession session) {  
           StringBuilder buffer = new StringBuilder(); 
           buffer.append("session=").append(session); 
           return buffer.toString(); 

Pruebas unitarias:

@Test 
        public void session() throws Exception { 
           this.mockMvc.perform(get("/data/standard/session"))  
                 .andExpect(content().string(startsWith( 
                      "session=org.springframework.mock.web.MockHttpSession@"))); 

u. Handler personalizado Handler

Es posible personalizar el handler de métodos para realizar operaciones antes de invocarlo.

Código en la página:

<a id="customArg" class="textLink" href="<c:URL 
        value="/data/custom" />">Customizado</a> 

En el controlador:

@ModelAttribute 
        void beforeInvokingHandlerMethod(HttpServletRequest request) { 
           request.setAttribute("foo", "bar"); 
        @RequestMapping(value="/data/custom", method=RequestMethod.GET) 
        public @ResponseBody String custom(@RequestAttribute("foo") String foo) { 
        return "Obtención del valor del atributo 'foo'=[" + foo + 
        "]"; 

v. Leer los datos codificados en la URL

Podemos leer los datos codificados en la URL.

Código en la página:

<form id="readForm" action="<c:url 
        value="/conversordemensajes/form" />" method="post"> 
        <input id="readFormSubmit" type="submit" value="Leer desde un Data" /> 
        </form> 

En el controlador:

// Desde un dato codificado (application/x-www-form-urlencoded) 
        @RequestMapping(value="/form", method=RequestMethod.POST) 
        public @ResponseBody String readForm(@ModelAttribute JavaBean bean) { 
           return "Lectura x-www-form-urlencoded: " + bean; 

Pruebas unitarias:

@Test 
        public void readForm() throws Exception { 
           this.mockMvc.perform( 
                 post(URI, "form")  
                    .contentType(MediaType.APPLICATION_FORM_URLENCODED) 
                    .param("foo", "bar") 
                    .param("fruit", "apple")) 
                 .andExpect(content().string("Lectura x-www-form- 
        urlencoded: JavaBean {foo=[bar], fruit=[apple]}")); 

w. Leer una estructura XML XML

Podemos recuperar los miembros de un objeto en forma de estructura XML.

Código en la página:

<form id="readXml" class="readXmlForm" action="<c:url 
        value="/conversordemensajes/xml" />" method="post"> 
        <input id="readXmlSubmit" type="submit" value="Leer una estructura XML" /> 
        </form> 

En el controlador:

// Jaxb => RootElement 
        // (requires JAXB2 on the classpath - useful for serving clients 
        that expect to work with XML) 
        @RequestMapping(value="/xml", method=RequestMethod.POSTpublic @ResponseBody String readXml(@RequestBody JavaBean bean) { 
        return "Lectura desde XML: " + bean; 

private static String XML = 
              "<?xml version=\"1.0\" encoding=\"UTF-8\" 
        standalone=\"yes\"?>" + 
              "<javaBean><foo>bar</foo><fruit>apple</fruit></javaBean>";  

x. Escribir en una estructura XML a través de Accept=application/xml

Podemos devolver una estructura XML que contenga los miembros de un objeto. 

Código en la página:

<a id="writeXmlAccept" class="writeXmlLink" href="<c:url 
        value="/conversordemensajes/xml" />">Escribir en una estructura 
        XML via Accept=application/xml</a> 

En el controlador:

@RequestMapping(value="/xml", method=RequestMethod.GETpublic @ResponseBody JavaBean writeXml() { 
           return new JavaBean("bar", "apple"); 

Pruebas unitarias:

@Test 
        public void readXml() throws Exception { 
           this.mockMvc.perform( 
                 post(URI, "xml")  
                    .contentType(MediaType.APPLICATION_XML) 
                    .content(XML.getBytes())) 
                 .andExpect(content().string("Lectura desde XML: 
        JavaBean {foo=[bar], fruit=[apple]}")); 

Código en la página:

<a id="writeXmlExt" class="writeXmlLink" href="<c:URL 
        value="/conversordemensajes/xml.xml" />">Write XML via 
        ".xml"</a> 

y. Leer una estructura JSON JSON

Podemos devolver una estructura JSON que contenga los miembros de un objeto. 

Código en la página:

<form id="readJson" class="readJsonForm" action="<c:url 
        value="/conversordemensajes/json" />" method="post"> 
        <input id="readJsonSubmit" type="submit" value="Leer una estructura JSON" /> 
        </form> 

En el controlador:

// Jackson => Mensaje 
        //(requiere Jackson en el classpath. Muy útil para los clientes 
        JavaScript que piden trabajar con el formato JSON@RequestMapping(value="/json", method=RequestMethod.POSTpublic @ResponseBody String readJson(@Valid @RequestBody JavaBean bean) { 
        return "Leer desde un estructura JSON: " + bean; 

Pruebas unitarias:

@Test 
        public void writeXml() throws Exception { 
           this.mockMvc.perform(get(URI, 
        "xml").accept(MediaType.APPLICATION_XML)) 
                 .andExpect(content().xml(XML)); 

Código en la página:

<form id="readJsonInvalid" class="readJsonForm invalid" 
        action="<c:url value="/conversordemensajes/json" />" 
        method="post"<input id="readInvalidJsonSubmit" type="submit" value="Leer una 
        estructura JSON inválida (código de respuesta 400)" /> 
        </form> 

El servicio devuelve 400 porque no es JSON.

z. Escribir en una estructura JSON a través de Accept=application/json

Podemos devolver una estructura JSON que contenga los miembros de un objeto. 

Código en la página:

<a id="writeJsonAccept" class="writeJsonLink" href="<c:url 
        value="/conversordemensajes/json" />">Escribir en una 
        estructura JSON a través de Accept=application/json</a> 

En el controlador:

@RequestMapping(value="/json", method=RequestMethod.GETpublic @ResponseBody JavaBean writeJson() { 
           return new JavaBean("bar", "apple"); 

Pruebas unitarias:

@Test 
        public void readJson() throws Exception { 
           this.mockMvc.perform( 
                 post(URI, "json")  
                    .contentType(MediaType.APPLICATION_JSON) 
                    .content("{ \"foo\": \"bar\", \"fruit\": \"apple\" 
        }".getBytes())) 
        .andExpect(content().string("Leer desde un JSON: JavaBean 
        {foo=[bar], fruit=[apple]}")); 

@Test 
        public void writeJson() throws Exception { 
           this.mockMvc.perform(get(URI, 
        "json").accept(MediaType.APPLICATION_JSON)) 
                 .andExpect(jsonPath("$.foo").value("bar")) 
                 .andExpect(jsonPath("$.fruit").value("apple")); 

aa. HTML generado por una plantilla JSP HTML JSP

Es posible crear una plantilla HTTP para las JSP.

Código en la página:

<a href="<c:url value="/views/html" />">HTML generado por un 
        template JSP</a> 

En el controlador:

@RequestMapping(value="html", method=RequestMethod.GETpublic String prepare(Model model) { 
           model.addAttribute("foo", "bar"); 
           model.addAttribute("fruit", "apple"); 
           return "views/html"; 

Pruebas unitarias:

@Test 
        public void htmlView() throws Exception { 
           this.mockMvc.perform(get("/views/html")) 
                 .andExpect(view().nombre(containsString("views/html"))) 
                 .andExpect(model().attribute("foo", "bar")) 
                 .andExpect(model().attribute("fruit", "apple")) 
                 .andExpect(model().size(2)); 

ab. Mapping a partir de un modelo

Podemos usar DefaultRequestToViewNameTranslator.

Esto es muy práctico porque permite hacer un mapping automático a partir de un modelo.

Código en la página:

<a href="<c:url value="/views/viewName" 
        />">DefaultRequestToViewNameTranslator convention</a> 

En el controlador:

@RequestMapping(value="/viewName", method=RequestMethod.GETpublic void usingRequestToViewNameTranslator(Model model) { 
           model.addAttribute("foo", "bar"); 
           model.addAttribute("fruit", "apple"); 

Pruebas unitarias:

@Test 
        public void viewName() throws Exception { 
           this.mockMvc.perform(get("/views/viewName"))  
                 .andExpect(view().nombre(containsString("views/viewName"))) 
                 .andExpect(model().attribute("foo", "bar")) 
                 .andExpect(model().attribute("fruit", "apple")) 
                 .andExpect(model().size(2)); 

ac. Usar variables en una plantilla de vista

Podemos usar variables en una plantilla de vista utilizando la anotación @PathVariable.

Código en la página:

<a href="<c:url value="/views/pathVariables/bar/apple" 
        />">Utilizar las variables en un template de vista </a> 

En el controlador:

@RequestMapping(value="pathVariables/{foo}/{fruit}", 
        method=RequestMethod.GET) 
        public String pathVars(@PathVariable String foo, @PathVariable 
        String fruit) { 
           // No need to add @PathVariables "foo" and "fruit" to the model 
           // They will be merged in the model before rendering 
           return "views/html"; 

Pruebas unitarias:

@Test 
        public void uriTemplate() throws Exception { 
           this.mockMvc.perform(get("/views/pathVariables/bar/apple")) 
                 .andExpect(view().nombre(containsString("views/html"))); 

ad. Data binding con variables de URI Data binding URI

También podemos enlazar automáticamente las variables de URI.

Código en la página:

<a href="<c:url value="/views/dataBinding/bar/apple" />">Data 
        binding con variables de URI</a> 

En el controlador:

@RequestMapping(value="dataBinding/{foo}/{fruit}", 
        method=RequestMethod.GETpublic String dataBinding(@Valid JavaBean javaBean, Model model) 
        { 
           // JavaBean "foo" and "fruit" properties populated from URI 
        variables 
        return "views/dataBinding"; 

ae. Tipos primitivos Tipos:primitivos

Es posible mapear simplemente los tipos primitivos, como las fechas. Los tipos primitivos a menudo plantean problemas de interoperabilidad. En el siguiente ejemplo, se muestra código anterior a Java 8 con Joda Time.

Código en la página:

<a id="primitive" class="textLink" href="<c:url 
        value="/convert/primitive?value=3" />">Primitivo</a> 

Clase utilizada para mostrar los diferentes tipos primitivos:

Código simplificado:

public class JavaBean { 
           private Integer primitive; 
           @DateTimeFormat(iso=ISO.DATE) 
           private Date date;  
           @MaskFormat("(###) ###-####") 
           private String masked; 
           // list will auto-grow as its dereferenced e.g. list[0]=value 
           private List<Integer> list; 
           // annotation type conversion rule will be applied to each 
        list element  
           @DateTimeFormat(iso=ISO.DATE) 
           private List<Date> formattedList; 
           // map will auto-grow as its dereferenced e.g. map[key]=value 
           private Map<Integer, String> map; 
           // nested will be set when it is referenced e.g. 
        nested.foo=value 
           private NestedBean nested; 
         
        @Target(value={ElementType.FIELD, ElementType.METHOD, 
        ElementType.PARAMETER}) 
        @Retention(RetentionPolicy.RUNTIME) 
        @Documented 
        public @interface MaskFormat { 
        String value(); 
         
        public class MaskFormatAnnotationFormatterFactory implements 
        AnnotationFormatterFactory<MaskFormat> { 
           public Set<Class<?>> getFieldTypes() { 
              Set<Class<?>> fieldTypes = new HashSet<Class<?>>(1, 1); 
              fieldTypes.add(String.class); 
              return fieldTypes; 
           } 
           public Parser<?> getParser(MaskFormat annotation, Class<?> 
        fieldType) { 
              return new MaskFormatter(annotation.value()); 
           } 
           public Printer<?> getPrinter(MaskFormat annotation, Class<?> 
        fieldType) { 
              return new MaskFormatter(annotation.value()); 
           } 
         
           private static class MaskFormatter implements 
        Formatter<String> { 
              private javax.swing.text.MaskFormatter delegate; 
              public MaskFormatter(String mask) { 
                 try { 
                    this.delegate = new 
        javax.swing.text.MaskFormatter(mask); 
         
        this.delegate.setValueContainsLiteralCharacters(false); 
                 } catch (ParseException e) { 
                    throw new IllegalStateException("Mask could not be 
        parsed " + mask, e); 
                 } 
              } 
              public String print(String object, Locale locale) { 
                 try { 
                    return delegate.valueToString(object); 
                 } catch (ParseException e) { 
                    throw new IllegalArgumentException("Unable to print 
        using mask " + delegate.getMask(), e); 
                 } 
              } 
              public String parse(String text, Locale locale) throws 
        ParseException { 
                 return (String) delegate.stringToValue(text); 
              } 
           } 
         
        public final class SocialSecurityNumber { 
           private final String value; 
         
           public SocialSecurityNumber(String value) { 
              this.value = value; 
           } 
         
           @MaskFormat("###-##-####") 
           public String getValue() { 
              return value; 
           } 
           public static SocialSecurityNumber valueOf(@MaskFormat("###- 
        ##-####") String value) { 
              return new SocialSecurityNumber(value); 
           } 
         
         
        @RequestMapping("primitive") 
        public @ResponseBody String primitive(@RequestParam Integer 
        value) { 
        return "Conversión del tipo primitivo " + value; 

Pruebas unitarias:

@Before 
        public void setup() throws Exception { 
           FormattingConversionService cs = new 
        DefaultFormattingConversionService(); 
           cs.addFormatterForFieldAnnotation(new 
        MaskFormatAnnotationFormatterFactory()); 
           this.mockMvc = standaloneSetup(new ConvertController()) 
                 .setConversionService(cs) 
                 .alwaysExpect(status().isOk()) 
                 .build(); 
         
        private String getTimezone(int year, int month, int day) 
           Calendar calendar = Calendar.getInstance(); 
           calendar.set(Calendar.YEAR, year); 
           calendar.set(Calendar.MONTH, month); 
           calendar.set(Calendar.DAY_OF_MONTH, day); 
           Date date = calendar.getTime(); 
           TimeZone timezone = TimeZone.getDefault(); 
           boolean inDaylight = timezone.inDaylightTime(date); 
           return timezone.getDisplayName(inDaylight, TimeZone.SHORT); 

@Test 
        public void primitive() throws Exception { 
           this.mockMvc.perform(get("/convert/primitive").param("value", "3")) 
        .andExpect(content().string("Conversión del tipo primitivo 3")); 

af. Fechas Fechas

Podemos decodificar fechas directamente:

Código en la página:

<a id="date" class="textLink" href="<c:URL 
        value="/convert/date/2010-07-04" />">Date</a> 

En el controlador:

// requires Joda-Time on the classpath 
        @RequestMapping("date/{value}"public @ResponseBody String date(@PathVariable 
        @DateTimeFormat(iso=ISO.DATE) Date value) { 
           return "Fecha convertida " + value; 

Pruebas unitarias:

@Test 
        public void date() throws Exception { 
           String timezone = getTimezone(2010, 7, 4); 
           this.mockMvc.perform(get("/convert/date/2010-07-04")) 
                 .andExpect(content().string("Fecha convertida Sun Jul 04 
        00:00:00 " + timezone + " 2010")); 

ag. Conversión de colecciones

A menudo, las colecciones son difíciles de transmitir. Spring permite convertirlas automáticamente.

Código en la página:

<a id="collection" class="textLink" href="<c:url 
        value="/convert/collection?values=1&values=2&values=3&values4&values=5" />">Colección 1 (multi-value parameter)</a> 

En el controlador:

@RequestMapping("collection"public @ResponseBody String collection(@RequestParam 
        Collection<Integer> values) { 
           return "Colección convertida " + values; 

Pruebas unitarias:

@Test 
        public void collection() throws Exception { 
           this.mockMvc.perform(get("/convert/collection?  
        values=1&values=2&values=3&values=4&values=5")) 
                 .andExpect(content().string("Colección convertida [1, 2, 
        3, 4, 5]")); 
         
        <a id="collection2" class="textLink" href="<c:url 
        value="/convert/collection?values=1,2,3,4,5" />">Colección 2 
        (single comma-delimited parameter value)</a> 

@Test 
        public void collection2() throws Exception { 
           this.mockMvc.perform(get("/convert/collection?values=1,2,3,4,5")) 
        .andExpect(content().string("Colección convertida [1, 2, 3, 4, 5]")); 

ah. Usar colecciones con formato

Podemos utilizar colecciones formateadas.

Código en la página:

<a id="formattedCollection" class="textLink" href="<c:url 
        value="/convert/formattedCollection?values=2010-07-04,2011-07-04" 
        />">@Formatted Collection</a> 

En el controlador:

@RequestMapping("formattedCollection"public @ResponseBody String formattedCollection(@RequestParam 
        @DateTimeFormat(iso=ISO.DATE) Collection<Date> values) { 
           return "Colección convertida formateada " + values; 

ai. Trabajar con objetos personalizados

También tenemos la posibilidad de utilizar objetos personalizados.

Código en la página:

<a id="valueObject" class="textLink" href="<c:url 
        value="/convert/value?value=123456789" />">Objeto personalizado</a> 

En el controlador:

@RequestMapping("value"public @ResponseBody String valueObject(@RequestParam 
        SocialSecurityNumber value) { 
        return "Valor del objeto convertido " + value; 

Pruebas unitarias:

@Test 
        public void valueOf() throws Exception { 
           this.mockMvc.perform(get("/convert/value?value=123456789")) 
        .andExpect(content().string(startsWith( 
                       "Valor del objeto convertido 
        com.eni.spring.mvc.exemple.convert.SocialSecurityNumber"))); 

aj. Usar un conversor personalizado

También tenemos los Converters para ayudarnos a cambiar el formato de los datos.

Código en la página:

<a id="customConverter" class="textLink" href="<c:url 
        value="/convert/custom?value=123-45-6789" />">Custom 
        Converter</a> 

En el controlador:

@RequestMapping("custom"public @ResponseBody String customConverter(@RequestParam 
        @MaskFormat("###-##-####") String value) { 
        return  "Convertido '"+ value +"' con un conversor personalizado"; 

Pruebas unitarias:

@Test 
        public void custom() throws Exception { 
           this.mockMvc.perform(get("/convert/custom?value=123-45-6789")) 
        .andExpect(content().string("Convertido '123456789' con un 
        conversor personalizado")); 

Código en la página:

<a id="primitiveProp" class="textLink" href="<c:url 
        value="/convert/bean?primitive=3" />">Primitivo</a> 

Pruebas unitarias:

@Test 
        public void beanPrimitive() throws Exception { 
           this.mockMvc.perform(get("/convert/bean?primitive=3")) 
                 .andExpect(content().string("Conversor JavaBean 
        primitive=3")); 

Código en la página:

<a id="dateProp" class="textLink" href="<c:url 
        value="/convert/bean?date=25-04-18" />">Fecha</a> 

Pruebas unitarias:

@Test 
        public void beanDate() throws Exception { 
           String timezone = getTimezone(2010, 7, 4); 
           this.mockMvc.perform(get("/convert/bean?date=2010-07-04")) 
                 .andExpect(content().string("Conversor JavaBean date=Sun 
        Jul 04 00:00:00 " + timezone + " 2010")); 

Código en la página:

<a id="maskedProp" class="textLink" href="<c:url 
        value="/convert/bean?masked=(205) 333-3333" />">Masked</a> 

Pruebas unitarias:

@Test 
        public void beanMasked() throws Exception { 
           this.mockMvc.perform(get("/convert/bean?masked=(205) 333-3333")) 
                 .andExpect(content().string("Conversor JavaBean 
        masked=2053333333")); 

Código en la página:

<a id="listProp" class="textLink" href="<c:url 
        value="/convert/bean?list[0]=1&list[1]=2&list[2]=3" />">List 
        Elements</a> 

Pruebas unitarias:

@Test 
        public void beanCollection() throws Exception { 
           this.mockMvc.perform(get("/convert/bean?list[0]=1&list[1]= 
        2&list[2]=3")) 
                 .andExpect(content().string("Conversor JavaBean list= 
        [1, 2, 3]")); 

Código en la página:

<a id="formattedListProp" class="textLink" href="<c:url 
        value="/convert/bean?formattedList[0]=2010-07- 
        04&formattedList[1]=2011-07-04" />">@Formatted List Elements</a> 

Pruebas unitarias:

@Test 
        public void beanFormattedCollection() throws Exception { 
           String timezone2010 = getTimezone(2010, 7, 4); 
           String timezone2011 = getTimezone(2011, 7, 4); 
           this.mockMvc.perform(get("/convert/bean?formattedList[0]= 
        25-04-18&formattedList[1]=25-04-18")) 
                 .andExpect(content().string( 
                       "Conversor JavaBean formattedList=[Sun Jul 04 
        00:00:00 " + timezone2010 
                          + " 2010, Mon Jul 04 00:00:00 " + timezone2011 
        + " 2011]")); 
         
        <a id="mapProp" class="textLink" href="<c:url value="/convert/bean? 
        map[0]=apple&map[1]=pear" />">Map Elements</a> 

@Test 
        public void beanMap() throws Exception { 
           this.mockMvc.perform(get("/convert/bean?map[0]=apple&map[1]=pear" 
        )) 
                 .andExpect(content().string("Conversor JavaBean 
        map={0=apple, 1=pear}")); 

Código en la página:

<a id="nestedProp" class="textLink" href="<c:url 
        value="/convert/bean?nested.foo=bar&nested.list[0].foo=baz&nested 
        .map[key].list[0].foo=bip" />">Nested</a> 

Pruebas unitarias:

@Test 
        public void beanNested() throws Exception { 
           this.mockMvc.perform(get("/convert/bean? 
        nested.foo=bar&nested.list[0].foo=baz&nested.map[key] 
        .list[0].foo=bip")) 
                 .andExpect(content().string( 
                       "Conversor JavaBean nested=NestedBean 
        foo=bar list=[NestedBean foo=baz] map={key=NestedBean 
        list=[NestedBean foo=bip]}")); 

Código en la página:

<a id="validateNoErrors" class="textLink" href="<c:url 
        value="/validate?number=3&date=2029-07-04" />">Validate, no 
        errors</a> 

Objeto para los ejemplos:

public class JavaBean { 
         
           @NotNull 
           @Max(5private Integer number; 
           @NotNull 
           @Future 
           @DateTimeFormat(iso=ISO.DATE) 
           private Date date; 
           public Integer getNumber() { 
              return number; 
           } 
           public void setNumber(Integer number) { 
              this.number = number; 
           } 
           public Date getDate() { 
              return date; 
           } 
           public void setDate(Date date) { 
              this.date = date; 
           } 

Pruebas unitarias:

@Before 
        public void setup() throws Exception { 
           this.mockMvc = standaloneSetup(new 
        ValidationController()).alwaysExpect(status().isOk()).build(); 
         
        @Test 
        public void validateSuccess() throws Exception { 
           this.mockMvc.perform(get("/validate?number=3&date=2029-07-04")) 
        .andExpect(content().string("Sin error ")); 

ak. Validación Validador

Se pueden utilizar validadores. Lo ideal es validar en todos los niveles para evitar problemas, es decir, en la página, el controlador, los servicios, las DAO y en la base de datos, si es posible con restricciones de integridad.

Código en la página:

<a id="validateErrors" class="textLink" href="<c:url 
        value="/validate?number=3&date=25-04-18" />">Validate, 
        errors</a> 

Es necesario agregar una gestión de excepciones del tipo Business.

@SuppressWarnings("serial"public class BusinessException extends Exception { 

En el controlador:

@ControllerAdvice 
        public class GlobalExceptionHandler { 
           @ExceptionHandler 
           public @ResponseBody String 
        handleBusinessException(BusinessException ex) { 
              return "Handled BusinessException"; 
           }  
         
        // Refuerzo de la restricción sobre los argumentos del JavaBean 
        que necesita una implementación de la JSR-303 en el classpath 
        @RequestMapping("/validate"public @ResponseBody String validate(@Valid JavaBean bean, 
        BindingResult result) { 
        if (result.hasErrors()) { 
              return "El objeto tiene errores de validación"; 
           } else { 
              return "Sin errores"; 
           } 

Pruebas unitarias:

@Test 
        public void validateErrors() throws Exception { 
           this.mockMvc.perform(get("/validate?number=3&date=25-04-18")) 
        .andExpect(content().string("El objeto tiene errores de 
        validación")); 

al. @ExceptionHandler en un controlador

Es posible tener un administrador de excepciones integrado en el controlador, usando la anotación @ExceptionHandler.

Código en la página:

<a id="exception" class="textLink" href="<c:URL 
        value="/exception" />">@ExceptionHandler en un controlador</a> 

En el controlador:

@RequestMapping("/exception"public @ResponseBody String exception() { 
           throw new IllegalStateException("Sorry!"); 

am. @ExceptionHandler global

Podemos tener un administrador de excepciones integrado en el controlador.

Usaremos la anotación @ExceptionHandler.

Código en la página:

<a id="globalException" class="textLink" href="<c:URL 
        value="/global-exception" />">@ExceptionHandler global</a> 

En el controlador:

@RequestMapping("/global-exception"public @ResponseBody String businessException() throws 
        BusinessException throw new BusinessException(); 

an. Plantillas de String para las URI URI

Podemos usar plantillas URI.

Código en la página:

<a href="<c:url value="/redirect/uriTemplate" />">String de 
        Template de URI</a> 

private final ConversionService conversionService; 
        @Inject 
        public RedirectController(ConversionService conversionService) this.conversionService = conversionService; 
         
        @RequestMapping(value="/uriTemplate", method=RequestMethod.GETpublic String uriTemplate(RedirectAttributes redirectAttrs) { 
           redirectAttrs.addAttribute("account", "a123");  // Utilizado como 
        una variable de template URI 
           redirectAttrs.addAttribute("date", new LocalDate(2011, 12, 31)); 
        // Añadido como un argumento de query 
           return "redirect:/redirect/{account}"; 

Pruebas unitarias:

@Before 
        public void setup() throws Exception { 
           this.mockMvc = standaloneSetup(new RedirectController(new 
        DefaultFormattingConversionService())) 
                 .alwaysExpect(status().isMovedTemporarily()).build(); 
         
        @Test 
        public void uriTemplate() throws Exception { 
           this.mockMvc.perform(get("/redirect/uriTemplate")) 
                 .andExpect(redirectedUrl("/redirect/a123? 

ao. UriComponentsBuilder UriComponentsBuilder

Tenemos a nuestra disposición el UriComponentsBuilder, que genera la URI para nosotros.

Código en la página:

<a href="<c:url value="/redirect/uriComponentsBuilder" 
        />">UriComponentsBuilder</a> 

En el controlador:

@RequestMapping(value="/uriComponentsBuilder", 
        method=RequestMethod.GET) 
        public String uriComponentsBuilder() { 
           String date = this.conversionService.convert(new 
        LocalDate(2011, 12, 31), String.class); 
           UriComponents redirectUri = 
        UriComponentsBuilder.fromPath("/redirect/{account}") 
        .queryParam("date", date) 
              .build().expand("a123").encode(); 
           return "redirect:" + redirectUri.toUriString(); 

Pruebas unitarias:

@Test 
        public void uriComponentsBuilder() throws Exception { 
           this.mockMvc.perform(get("/redirect/uriComponentsBuilder")) 
                 .andExpect(redirectedUrl("/redirect/a123?date=12/31/11")); 
         
        @RequestMapping(value="/{account}", method=RequestMethod.GET) 
        public String show(@PathVariable String account, 
        @RequestParam(required=false) LocalDate date) { 
        return "redirect/redirectResults"; 

Cliente REST REST

El cliente REST se beneficia de las facilidades ofrecidas por el template RestTemplate.

1. Utilización del RestTemplate RestTemplate

Un cliente REST es bastante sencillo con Spring.

@Log 
        public class ClienteRest { 
           public static void main(String[] args) { 
              ApplicationContext ctx = new ClassPathXmlApplicationContext( 
                       new String[] { "applicationContext.xml" }); 
              RestTemplate restTemplate = new RestTemplate(); 
              Usuario usuario= restTemplate.getForObject( 
                       "http://localhost:8080/cap09-sr2/usuarios/2", 
                       Usuario.class); 
              log.info(usuario.toString()); 
           } 
        } 

2. El bean de dominio con la anotación REST para los campos ausentes

Es posible ignorar los campos ausentes.

Podemos utilizar estos campos para administrar la compatibilidad con versiones anteriores.

@JsonIgnoreProperties(ignoreUnknown = true@Getter @Setter @ToString 
        @NoArgsConstructor 
        @AllArgsConstructor 
        public class Usuario { 
        private long id; 
            private String apellido; 
        private String nombre; 

Spring Security Spring Security

1. Introducción a Spring Security

Spring Security ayuda a proteger las aplicaciones a diferentes niveles. Spring utiliza aspectos (AOP) para interceptar métodos y añadir un contexto de seguridad. 

images/cap9_pag55.png

Se articula en torno a cuatro conceptos:

Principal

El usuario, sistema o dispositivo que realiza una acción.

Autenticación

Verificación de la validez de los derechos de main.

Autorización

Verificación de que la acción está permitida para main.

Recurso seguro

El recurso para el que se deben verificar los derechos de acceso.

Existen varios mecanismos de autenticación basados en:

  • un mecanismo ultrabásico,

  • un resumen,

  • un formulario,

  • un acceso X-509 (LDAP, por ejemplo),

  • cookies,

  • SSO (Single Sign-On).

Cuando sea posible, es recomendable utilizar SSO para reutilizar las credenciales de la sesión de la estación de trabajo.

Para aplicaciones muy sensibles, es posible utilizar un lector de tarjetas inteligentes o un sistema de tokens (sistema basado en un número que se muestra en una caja o calculadora, el cual cambia cada x segundos).

Esta autenticación se puede asociar con derechos que se registran:

  • en duro en un programa durante el ciclo de desarrollo,

  • en un archivo .properties,

  • en una base de datos (SQL, NoSQL a través de un DAO clásico),

  • en una base de datos LDAP,

  • en un sistema externo dedicado.

Para determinar si una acción es posible, primero es necesario verificar quién está haciendo la consulta utilizando el mecanismo de autenticación. Podemos definir que un usuario que aún no está identificado se considera un invitado (GUEST). El sistema más sencillo puede considerar a un usuario estándar como miembro (MEMBER) si se trata de un usuario que no tiene permisos de administración. Un usuario administrador tiene un rol de administrador (ADMIN). Estos tres roles, GUEST, MEMBER, ADMIN, son los roles básicos en Spring Security.

Veremos que también está previsto poder definir otros roles que complementarán estos roles sencillos. Spring Security es independiente de los mecanismos estándar utilizados en los servidores de aplicaciones. De esta manera, se hace portable de un servidor a otro. Sin embargo, todavía es posible conectar Spring Security a un tipo de proveedor de seguridad (security provider). Perdemos la portabilidad, pero luego unificamos el mecanismo en relación con el servidor.

Spring Security es muy flexible y permite personalizar todos los elementos relacionados con la autenticación y los derechos asociados.

Es posible utilizar Spring Security en las diferentes capas de aplicación:

  • Filtro de servlets,

  • Spring AOP para un enfoque basado en aspectos,

  • Sobre DAO y servicios,

  • ...

Los mecanismos de autenticación y autorización están desacoplados para simplificar el mantenimiento. La autenticación proporciona el contexto de seguridad. La autorización recupera los atributos del recurso seguro, recupera información sobre lo que está conectado (persona, dispositivo o aplicación) a partir del contexto de seguridad y determina si el acceso es válido o no.

2. Spring Security en un entorno web

Declaramos un filtro de servlet e indicamos lo que se filtrará:

Código en la página:

<filter> 
        <filter-name>springSecurityFilterChain</filter-name> 
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy 
        </filter-class> 
        </filter> 
        <filter-mapping> 
        <filter-name>springSecurityFilterChain</filter-name> 
        <URL-pattern>/*</URL-pattern> 
        </filter-mapping> 

Solo falta definir la configuración Spring con el namespace Security.

<intercept-url pattern="/admin**" access="ROLE_ADMIN" /> 
        </http> 

Las URL se interceptan en el orden en que se declaran.

a. Autenticación por Spring

Este es el mecanismo más sencillo, pero solo se muestra como ejemplo porque su uso es demasiado básico. Utilizamos el authentication-manager con el authentication-provider y el user-service:

<authentication-provider> 
        <user-service> 
        <user name="admin" password="admin" authorities="ROLE_ADMIN" /> 
        </user-service> 
        </authentication-provider> 
        </authentication-manager> 

b. Autenticación por página de inicio de sesión personalizada

He aquí un ejemplo un poco más complejo, que tampoco se usa porque es demasiado simplista. Especificamos la página que lleva el destino de inicio de sesión con el form-login.

<http auto-config="true"> 
        <intercept-url pattern="/admin**" access="ROLE_ADMIN" /> 
        <form-login 
                login-page="/login" 
                default-target-url="/welcome" 
              authentication-failure-url="/login?error"  
              username-parameter="username" 
              password-parameter="password" /> 
        <logout logout-success-url="/login?logout"  /> 
        <!-- enable csrf protection --> 
        <csrf/> 
        </http> 

c. Autenticación por base de datos

Agregamos un dataSource clásico:

<bean id="dataSource" 
           class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 
        <property name="driverClassName" value="org.h2.Driver" /> 
        <property name="url" value="jdbc:h2:~/testsecu" /> 
        <property name="username" value="sa" /> 
        <property name="password" value="sa" /> 
        </bean> 

Utilizamos un authentication-provider:

<authentication-manager> 
        <authentication-provider> 
        <jdbc-user-service data-source-ref="dataSource" 
                 users-by-username-query= 
                    "select username,password, enabled from users where 
        username=?" 
                 authorities-by-username-query= 
                    "select username, role from user_roles where 
        username =?  " /> 
        </authentication-provider> 
        </authentication-manager> 

Creamos una página 403:

<html> 
        <body> 
        <h1>HTTP Status 403 - Access is denied</h1> 
        <c:choose> 
        <c:when test="${empty username}"> 
        <h2>You do not have permission to access this page!</h2> 
        </c:when> 
        <c:otherwise> 
        <h2>Username : ${username} <br/>You do not have permission to 
        access this page!</h2> 
        </c:otherwise> 
        </c:choose> 
        </body> 
        </html> 

Creamos una página accesible para usuarios identificados:

<html> 
        <body> 
        <h1>Title : ${title}</h1> 
        <h1>Message : ${message}</h1>  
        <sec:authorize access="hasRole('ROLE_USER')"> 
        <!-- For login user --> 
        <c:URL value="/j_spring_security_logout" var="logoutUrl" /> 
        <form action="${logoutUrl}" method="post" id="logoutForm"> 
        <input type="hidden" name="${_csrf.parameterName}" 
                    value="${_csrf.token}" /> 
        </form> 
        <script> 
                 function formSubmit() { 
                    document.getElementById("logoutForm").submit(); 
                 } 
        </script> 
        <c:if test="${pageContext.request.userPrincipal.name != null}"> 
        <h2> 
                    User : ${pageContext.request.userPrincipal.name} | <a 
                       href="javascript:formSubmit()"> Logout</a> 
        </h2> 
        </c:if>  
        </sec:authorize> 
        </body> 
        </html> 

Puntos clave

  • Spring MVC es muy potente y permite hacer muchas cosas.

  • Spring MVC con JSP ha logrado sustituir a Struts 1.

  • Podemos probar la mayoría de nuestro código.

  • El modo MVC estandariza la forma de hacer la aplicación.

Introducción JSF2

En el día a día de su trabajo habitual, puede que tenga que utilizar aplicaciones JSF2. JSF2 es el estándar que sustituye a las páginas JSP. Sin embargo, todavía se proporciona con Jakarta EE 9 con la versión JSF 3.0 (https://jakarta.ee/specifications/faces/). La versión 4.0 está en desarrollo para la versión Jakarta EE 10.

En este capítulo solo abordamos JSF2, para permitir que el lector realice el mantenimiento o pueda intervenir en proyectos de migración. No hay más proyectos nuevos que usen Spring junto con las nuevas versiones de JSF (3 y 4).

Únicamente trataremos JSF2 de forma parcial y solo recordaremos los elementos principales para entender cómo integrarlo con Spring. De hecho, JSF2 es un conjunto bastante complejo que requeriría un libro entero para describir todos los detalles de su funcionamiento.

Primero veremos la implementación estándar de Mojorra. En la actualidad observamos cierta tendencia a la migración de aplicaciones JSF2 a Angular. De hecho, las nuevas aplicaciones suelen utilizar un framework Single Page Application como Angular, ReactJS o VueJS. El uso de Angular con Spring se explica en el capítulo Application Spring Angular.

JSF estaba muy bien adaptado cuando queríamos migrar aplicaciones cliente/servidor para las que queríamos mantener exactamente la misma interfaz hombre-máquina. Esto permite no tener que volver a formar a todos los usuarios. De hecho, es posible hacer pestañas complejas, ventanas modales y no modales y, sobre todo, gestionar secuencias de pantallas que permanecen en una transacción con switchs de contexto, es decir, poder modificar, por ejemplo, dos contratos comerciales en paralelo y guardarlos por separado en dos pestañas de la misma aplicación. Podemos hacer aplicaciones Statefull con sesiones muy grandes. Se trata de aplicaciones con pocos usuarios, pero con datos muy complejos. Modificamos un gran volumen de objetos, en varias pantallas, en las que persistimos a la vez.

Este tipo de aplicaciones siguen siendo complejas de hacer con las SPA.

Otro criterio para adoptar JSF2 es su uso controlado de JavaScript. De hecho, durante la adopción masiva de JSF como sustituto de JSP que se estaba volviendo obsoleta, los frameworks de JavaScript se consideraron inseguros y muy complejos, y los navegadores presentaban grandes diferencias entre ellos, que fueron administradas directamente por JSF2 y sus derivados. Había muchos componentes gráficos disponibles. No tenía que ser un experto en HTML5/CSS3/JavaScript para hacer que las páginas fueran complejas y seguras.

El uso de herramientas de generación de código como Celerio permite generar aplicaciones muy grandes (https://github.com/jaxio/pack-jsf2-spring-conversation) muy rápidamente, manteniendo (o no) el esquema de la base de datos original (y los datos que contiene).

El paquete todavía está disponible para su descarga.

A veces tendrá que trabajar en aplicaciones antiguas para mantenerlas, hacerlas evolucionar o modernizarlas. Algunas de ellas utilizan JSF2. JSF 2.3 está incluido en Jakarta EE 8.

Mojarra Mojarra

Mojarra es la implementación estándar. JSF2 reemplaza a las JSP para tener un enfoque de componentes.

La idea principal es exponer directamente los beans JSF «gestionados» en las vistas. Del mismo modo, se simplifica la noción de controlador. El objetivo es ocultar la complejidad, pero esto sigue siendo bastante delicado de implementar.

Mojarra está disponible en la dirección https://github.com/javaserverfaces/mojarra. Hay disponible una versión más moderna en https://github.com/eclipse-ee4j/mojarra

La última versión 2 requiere:

  • Java 1.8

  • Servlet 3.0 (se recomienda 4.0)

  • EL 3.0

  • CDI 1.2 (se recomienda 2.0) o Spring

  • JSTL 1.2

  • JSONP 1.1 (si se utiliza <f:websocket>)

  • BV 1.1 (si se utiliza <f:validateBean> o <f:validateWholeBean>, se recomienda la versión 2.0).

images/cap10_pag4.png

Arquitectura

La configuración se basa en el archivo web.xml y el archivo faces-config.xml. web.xml

Las últimas versiones 2 de los frameworks permiten prescindir de estos archivos a través de anotaciones, como veremos en un ejemplo.

De forma simplificada:

  • La consulta llega al controlador.

  • El controlador valida los datos.

  • Si hay errores, se devuelve la página actual con los errores.

  • De lo contrario, busca la vista correspondiente a la respuesta.

  • La vista consulta los beans gestionados presentes en la página; estos beans son datos de negocio o servicios.

  • La capa de negocio o DAO devuelve los datos a la página.

  • A continuación, se representa la página.

Es posible cortocircuitar los pasos durante las llamadas AJAX. Hay que tener en cuenta que hay un contexto en el servidor. Este contexto está relacionado con la página. Contiene la estructura de la página a nivel de los componentes con los datos de negocio (servicios y datos). La página cambia el contexto y, a continuación, lo devuelve. El contexto devuelto puede ser válido o no válido, en términos de validez de los datos modificados. Los datos solo se pueden guardar si el modelo es válido.

Será necesario evitar tener rollbacks provenientes de una interrupción durante la actualización en la base porque corrompe el contexto.

El contexto es de tipo FacesContext y contiene toda la información necesaria para el ciclo de vida de la consulta.

Incluye:

  • los componentes,

  • los datos de la vista,

  • los errores.

Las páginas están en XHTML y se pueden derivar de una composición de páginas. En general, se componen de una plantilla de página en la que se insertan los componentes como si fueran fragmentos de páginas. A nivel de transacción para los datos, es posible utilizar una conversación corta (ModelInView) o larga, usando un contexto extendido.

Arquitectura:

images/cap10_pag6.png

Ciclo de vida Ciclo de vida

El ciclo de vida describe las etapas realizadas por el controlador. Activando el log o registro de actividad, es posible trazar en qué etapa nos encontramos para un tratamiento determinado.

Ciclo de vida:

images/cap10_pag7.png

Los términos están en inglés porque aparecen así en la configuración y los logs.

1. Consulta

Se trata de una consulta HTTP. La consulta contiene los datos necesarios que JSF necesita para gestionar su ciclo de vida. Incluye elementos sobre el contexto asociado a la consulta.

2. Restore View o Reconstruct Component Tree

Desde la sesión, el servidor devuelve el contexto asociado con la sesión y recompone la estructura de árbol de los componentes de la página.

3. Apply Request Value

De la consulta HTTP se extraen los valores de los datos que se corresponden con los componentes. Utilizamos convertidores para adaptar el formato de los datos que se pasan en la consulta como cadenas de caracteres. Así que tendremos que crear convertidores.

4. Perform Validation

Esta es una fase crucial: vamos a utilizar los validadores guardados en los componentes para validar los datos en la página actual. Con JSF2 es posible validar los datos en las páginas antes de hacer el submit (enviar) del formulario. Los datos del lado servidor todavía se deben validar para evitar problemas.

A menudo, la validación de los datos se puede delegar en una parte al servicio para los asuntos del negocio y, en otra, al ORM de la capa de dominio para la validación directa en los beans del modelo. La validación puede provocar una discrepancia entre el contexto correspondiente a la página y el contexto JSF del servidor, que está en línea con el contexto de ORM. A continuación, es necesario asegurarse de que todos los datos se validan antes de solicitar una actualización. 

Los datos se pueden validar parcialmente para los datos presentes en varias vistas. A continuación, validamos solo los datos de la vista que mostramos.

En caso de error, renderizamos inmediatamente la página actual a través de la etapa «Render Response», con los errores en el contexto.

Si no hay ningún error, continuamos el ciclo.

5. Synchronize Model o Update Model Values

Como no hay errores, el contexto JSF se actualiza con los datos «extraídos» de la vista. Este es un progreso considerable porque anteriormente el binding se tenía que hacer de forma manual. El binding es automático y utiliza los convertidores de tipos según sea necesario para cambiar del formato que se muestra en la página al formato de la variable, que corresponde con los datos en la base de datos.

6. Invoke Application Logic

Durante esta etapa, se dispone de datos válidos para el procesamiento. Procesamos datos en forma de eventos. Durante este paso es cuando se determina qué página se mostrará para la respuesta.

7. Render Response

Desde el nombre de la página, cargamos la página y posicionamos los campos. A continuación, hacemos que se muestre la página correspondiente.

8. Respuesta

La página se devuelve en forma de un flujo HTML. Algunas partes de la página también se pueden representar a través de una llamada AJAX.

9. Archivo web.xml web.xml

El archivo web.xml está configurado para que el servlet FacesServlet se utilice para procesar consultas.

<servlet> 
           <servlet-name>Faces Servlet</servlet-name> 
           <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> 
           <load-on-startup>1</load-on-startup> 
        </servlet> 
        <servlet-mapping> 
           <servlet-name>Faces Servlet</servlet-name> 
           <url-pattern>*.jsf</url-pattern> 
        </servlet-mapping> 

10. Dependencias

Para pasar de un proyecto que utiliza JSP a otro que usa JSF, añadimos una dependencia a jsf-api y jsf-impl en el archivo pom.xml.

<dependency>  
           <groupId>com.sun.faces</groupId> 
           <artifactId>jsf-api</artifactId> 
           <version>${jsf.version}</version> 
           <scope>runtime</scope> 
        </dependency> 
        <dependency>  
           <groupId>com.sun.faces</groupId> 
           <artifactId>jsf-impl</artifactId> 
           <version>${jsf.version}</version> 
           <scope>runtime</scope> 
        </dependency> 

Especificamos el scope runtime. Esto indica que la dependencia es necesaria para la compilación, pero se proporcionará por el entorno. Habríamos utilizado provided si la librería no se hubiera requerido para la compilación.

11. Archivo faces-config.xml faces-config.xml

Este archivo describe información relativa a JSF como, por ejemplo:

  • la lista de beans gestionados,

  • las enumeraciones,

  • los parámetros de inicialización,

  • los mapas de las lists y de las properties,

  • los mensajes de error personalizados,

  • los validadores y convertidores personalizados,

  • las reglas de navegación,

  • una representación personalizada (tema),

  • los componentes personalizados,

  • etc.

Ejemplo de un archivo inicial faces-config.xml:

<?xml version="1.0" encoding="UTF-8"?> 
        <faces-config 
           xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
           http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd" 
           version="2.2"> 
        </faces-config> 

Por el momento, este archivo está vacío, pero podremos especificar elementos adicionales para JSF en él.

Ejemplo de archivo faces-config.xml con la navegación:

<?xml version="1.0"? encoding="UTF-8"?> 
          <faces-config ...> 
            <navigation-rule> 
            <from-view-id>/start-page-1.xhtml</from-view-id> 
            <navigation-case> 
              <from-outcome>too-short</from-outcome> 
              <to-view-id>/error-message.xhtml</to-view-id> 
            </navigation-case> 
            <navigation-case> 
              <from-outcome>page1</from-outcome> 
              <to-view-id>/result-page-1.xhtml</to-view-id> 
            </navigation-case> 
        [...] 
            <navigation-case> 
          </navigation-rule> 
        </faces-config> 

En los ejemplos descargables hay un ejemplo más completo.

12. Bean gestionado sin Spring Bean

JSF permite administrar beans. Estos beans sirven como interfaz entre los datos en las páginas y los datos en la parte back.

import java.util.Date; 
        import javax.faces.bean.ManagedBean; 
        import javax.faces.bean.RequestScoped;  
        @ManagedBean(name="usuario"@RequestScoped 
        public class Usuario { 
           private long id; 
           private String nombre; 
           private String apellido; 
           private Date fechaNacimiento; 
           public String save(){ 
              // Procesamiento 
              System.out.println(nombre); 
              System.out.println(fechaNacimiento); 
              return "success"; 
           } 
        [Getters et Setters] 

13. Ejemplo de vista JSF JSF

En general, la vista JSF es un archivo XHTML y, a menudo, se compone de fragmentos.

Utilizaremos un conjunto de plantillas que fijarán la disposición de los fragmentos de la vista. Esta plantilla irá acompañada de un conjunto de fragmentos reutilizables, que permitirán fabricar una vista personalizada.

  • webapp/WEB-INF/templates/templates.xhtml

  • webapp/mivista.xhtml

La vista declara un componente visual como un fragmento:

<ui:composition> 
           <ui:define name="content"> 
           </ui:define> 
        </ui:composition> 

A continuación, la plantilla incluye el componente o fragmento:

<h:panelGroup id="content" layout="block"> 
           <ui:insert name="content">Main Content</ui:insert> 
        </h:panelGroup> 

Se debe tener en cuenta que los fragmentos se inyectarán en la página. Esta no es una inclusión clásica. El fragmento etiquetado se colocará en la ubicación definida por una etiqueta de colocación <ui:insert />.

14. Vista previa de un componente JSF

El componente parece un DOM HTML, pero con una parte dinámica.

images/cap10_pag14.png

Los elementos de la parte front (en el navegador web) están relacionados con los datos de los beans gestionados.

Integración Spring

El bean gestionado por JSF se convierte en un bean Spring.

@Component("messageBackingBean"@Scope("request") 
        public class MessageBackingBean { 
           private Message message = new Message(); 
           private List<Message> messages; 
           @Autowired 
           private MessageDao messageDao; 
         
           public String getMessage() { 
              return "Hola"; 
           } 
           public Message getMessage() { 
              return message; 
           } 
           public void saveMessage() { 
              messageDao.save(message); 
              message = new Message(); 
              invalidateMessages(); 
           } 
           private void invalidateMessages() { 
              messages = null; 
           } 
           public List<Message> getMessages() { 
              if (messages == null) { 
                 messages = messageDao.list(); 
              } 
              return messages; 
         
           } 

1. Arquitectura

images/cap10_pag15.png

2. Dependencias

No hay dependencias adicionales para utilizar JSF2.

3. Archivo web.xml web.xml

Indicamos en este archivo los elementos que se desviarán para Spring.

El contexto se carga a través de ContextLoaderListener.

<context-param> 
           <param-name>contextConfigLocation</param-name> 
           <param-value>/WEB-INF/spring/application-context.xml</param-value> 
        </context-param> 
        <context-param> 
           <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name> 
           <param-value>true</param-value> 
        </context-param> 
        <context-param> 
           <param-name>  
        javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL 
           </param-name> 
           <param-value>true</param-value> 
        </context-param> 
        <context-param> 
           <param-name>javax.faces.PROJECT_STAGE</param-name> 
           <param-value>Development</param-value> 
        </context-param> 
        <context-param> 
           <param-name>javax.faces.PARTIAL_STATE_SAVING</param-name> 
           <param-value>false</param-value> 
        </context-param> 
        <listener> 
           <listener-class>  
        org.springframework.web.context.ContextLoaderListener 
        </listener-class> 
        </listener> 

4. Archivo faces-config.xml faces-config.xml

Especificamos que usamos el lenguaje de Spring para las expresiones de la página, indicando SpringBeanFacesELResolver en el argumento el-resolver:

<faces-config 
            xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
            xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee/ 
        web-facesconfig_2_2.xsd" 
            version="2.2"> 
         
           <application>  
              <el-resolver> 
               org.springframework.web.jsf.el.SpringBeanFacesELResolver 
              </el-resolver> 
              <locale-config> 
              </locale-config> 
              <resource-bundle> 
                 <base-name>messages</base-name> 
                 <var>msg</var> 
              </resource-bundle> 
           </application> 
        </faces-config> 

5. Capas inferiores (back)

Mantenemos los datos viables para un ciclo de vida usando OpenEntityManagerInViewFilter:

<filter> 
           <filter-name>hibernateFilter</filter-name> 
           <filter-class> 
              org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter 
        </filter-class> 
        </filter> 
        <filter-mapping> 
           <filter-name>hibernateFilter</filter-name> 
           <servlet-name>Faces Servlet</servlet-name> 
        </filter-mapping> 

Sería posible tener una conversación en lugar de una duración limitada a un intercambio.

Puntos clave

  • JSF2 es muy simple de usar.

  • Spring interactúa fácilmente con JSF2.

  • Podemos usar JSF2 para aplicaciones Spring MVC.

Introducción

Angular se utiliza cada vez más para la parte front de aplicaciones, algunas veces con ayuda de componentes ReactJS o VueJS, en lugar de o en sustitución de las páginas JSP o JSF. Las tecnologías de representación visual de servidores basadas en plantillas como JSF o JSP prácticamente ya no se utilizan para crear nuevas aplicaciones. ReactJS JSP JSF

Empezar con Angular es complejo. Para entender cómo funciona, lo ideal para este capítulo es probar los ejemplos al mismo tiempo que se lee la teoría. Si desea dominar este framework, puede consultar el libro Angular - Desarrolle sus aplicaciones web con el framework JavaScript de Google, de Daniel DJORDJEVIC, Sébastien OLLIVIER y William KLEIN, publicado por Ediciones ENI.

Este capítulo se ilustra con dos pequeños proyectos. Un proyecto Front en Angular y uno back en Spring Boot. Angular

La parte front es estática a nivel de archivos y, por lo tanto, se puede desplegar directamente en los front-ends Web (Apache, Nginx, etc.). Apache

El ejemplo es deliberadamente sencillo para ilustrar los aspectos principales. Para una aplicación completa, puede crear y estudiar fácilmente ejemplos con jHipster.

El ejemplo utiliza Java 8, NodeJS y Angular CLI. NodeJS Angular CLI

La parte backend

La parte backend es una aplicación sencilla que expone una API REST.

1. Generación de un backend

Vamos a crear un backend clásico con Spring Initializr (https://start.spring.io/): Spring Initializr

Metadatos del proyecto:

Parámetro

Utilidad

type

maven/java/Spring Boot 2.5.12

group

fr.eni.spring5.angular

artefact

fr-eni-spring5-backend

Description

Proyecto ejemplo Angular

Los módulos Spring del proyecto:

Módulo

Utilidad

DevTools

Facilitar la recarga del servidor en caso de recompilación

Lombok

Para simplificar el código

H2

Para la base de datos

JPA

Para la capa de persistencia

Rest Repositories

Para exponer los servicios REST

Web

Tener una aplicación web

Rest Repositories HAL Browser Rest Repositories HAL Browser

Para ver datos en un explorador

Una vez que se genera el proyecto, es posible arrancar el servidor inicial con el comando mvn spring-boot:run. spring-boot\\:run

La página del navegador HAL se muestra en http://localhost:8080/ browser/index.html#/. Navegador HAL

Vamos a crear los paquetes en fr.eni.spring5.angular.backend:

Paquete

Utilidad

domain

Objetos de dominio JPA

repositories

DAO para acceder a objetos de dominio

controllers

Controladores Spring MVC

config

Las clases de configuración Spring

util

Clases de utilidades

Vamos a crear una primera clase de dominio:

Cuenta.java

@Data 
        @NoArgsConstructor 
        @EqualsAndHashCode(exclude={"id"}) 
        @Entity 
        public class Cuenta { 
          @Id 
          @GeneratedValue 
          private Long id; 
          private Integer numero; 
          private String nombre; 
         
          public Cuenta(Integer numero, String nombre) { 
            this.id=null; 
            this.numero=numero; 
            this.nombre=nombre; 
          } 
        } 

Utilizamos Lombok y solicitamos la generación de métodos equals y hashcode, excluyendo la variable id gestionada por JPA.

Añadimos un @RepositoryRestResource para crear el DAO y la API REST.

La interfaz CuentaRepository hereda de JpaRepository:

@RepositoryRestResource 
        public interface CuentaRepository extends JpaRepository<Cuenta, Long> { 
        } 

Añadimos el controlador:

La clase CuentaController

@RestController 
        public class CuentaController { 
          private CuentaRepository repository; 
         
          public CuentaController(CuentaRepository repository) { 
            this.repository = repository; 
          } 
         
          @GetMapping("/lascuentas") 
          @CrossOrigin(origins = "http://localhost:4200") 
          public List<Cuenta> getCuentas() { 
            return repository.findAll(); 
          } 
         
        } 

Finalmente, añadimos el lanzador de aplicaciones y su clase, que implementa CommandLineRunner:

La clase del lanzador

@SpringBootApplication 
        public class BackendApplication { 
         
            public static void main(String[] args) { 
                SpringApplication.run(BackendApplication.class, args); 
            } 
        } 

La clase que implementa CommandLineRunner

@Component 
        public class CuentaCommandLineRunner implements CommandLineRunner { 
          private static final Logger LOGGER = 
        LoggerFactory.getLogger(CuentaCommandLineRunner.class); 
          private final CuentaRepository repository; 
         
          public CuentaCommandLineRunner(CuentaRepository repository) { 
            this.repository = repository; 
          } 
         
          @Override 
          public void run(String... strings) throws Exception { 
            repository.save(new Cuenta(2741, "Préstamos de capital ")); 
            repository.save(new Cuenta(2742, "Préstamos asociados ")); 
            repository.save(new Cuenta(2743, "Préstamos personales ")); 
            repository.findAll().forEach(System.out::println); 
          } 
        } 

La parte frontend

La parte frontend es relativamente genérica e independiente de la parte backend.

Para el frontend, utilizamos las herramientas Angular CLI que simplifican nuestro desarrollo, usando la línea de comandos. Angular CLI

Asumiremos que las siguientes herramientas ya están instaladas:

Herramientas

Sitio de referencia para la instalación

Node.js

https://nodejs.org/en/

yarn yarn

https://yarnpkg.com/en/

1. Angular CLI

Angular CLI es una suite de herramientas en línea de comandos, que ayuda a crear y modificar aplicaciones Angular.

A continuación, se muestra una tabla que enumera los comandos:

Comando

Utilidad

ng new ng new

Crear una nueva aplicación.

ng serve

Iniciar el servidor.

ng generate

Generar un componente, directiva, ruta, pipe o servicio.

ng lint

Lint el código de la aplicación con tslint.

ng test

Ejecutar las pruebas unitarias.

ng e2e

Ejecutar las pruebas end to end.

ng build

Construir la aplicación.

2. Creación del proyecto inicial

Instalar Angular CLI:

npm install -g @angular/cli 

Crear el proyecto Angular:

ng new miApli --directory. 

La descarga de los archivos se inicia en el directorio actual.

3. Inicio de la aplicación

ng serve 

La aplicación se inicia y es visible en http://localhost:4200/

Es posible terminar la ejecución con la combinación de teclas [Ctrl] C.

Cree un componente CuentaListComponent y otro CuentaService con el siguiente comando:

ng generate component cuenta-list 
          create src/app/cuenta-list/cuenta-list.component.css (0 bytes) 
          create src/app/cuenta-list/cuenta-list.component.html (30 bytes) 
          create src/app/cuenta-list/cuenta-list.component.spec.ts (657 bytes) 
          create src/app/cuenta-list/cuenta-list.component.ts (288 bytes) 
          update src/app/app.module.ts (416 bytes) 

4. Crear un servicio Cuenta

Necesitamos crear un servicio Angular Cuenta.

ng g s cuenta 
          create src/app/cuenta.service.spec.ts (374 bytes) 
          create src/app/cuenta.service.ts (112 bytes) 

Cree un directorio src/app/shared/cuenta. Mueva a este directorio el servicio cuenta.service y su prueba cuenta.service.spec.ts.

Cree un índice src/app/shared/index.ts y añada esta línea:

export * from './cuenta/cuenta.service'; 

Modifique el servicio src/app/shared/cuenta/cuenta.service.ts:

import { Injectable } from '@angular/core'import { HttpClient } from '@angular/common/http'import { Observable } from 'rxjs/Observable'; 
         
        @Injectable() 
        export class CuentaService { 
         
          constructor(private http: HttpClient) {} 
         
          getAll(): Observable<any> { 
            return this.http.get('http://localhost:8080//cuenta-service'); 
          } 
        } 

Añada la importación HttpClientModule a src/app/app.module.ts.

import { BrowserModule } from '@angular/platform-browser'; 
        import { NgModule } from '@angular/core'; 
        import { HttpClientModule } from '@angular/common/http'; 
         
        import { AppComponent } from './app.component'; 
        import { CuentaListComponent } from './cuenta-list/ 
        cuenta-list.component'; 
         
         
        @NgModule({ 
          declarations: [ 
            AppComponent, 
            CuentaListComponent 
          ], 
          imports: [ 
            BrowserModule, 
            HttpClientModule 
          ], 
          providers: [], 
          bootstrap: [AppComponent] 
        }) 
        export class AppModule { } 

Modifique src/app/cuanta-list/cuenta-list.component.ts para usar CuentaService y coloque el resultado en una variable local.

import { Component, OnInit } from '@angular/core'import { CuentaService } from '../shared'; 
         
        @Component({ 
          selector: 'app-cuenta-list', 
          templateUrl: './cuenta-list.component.html', 
          styleUrls: ['./cuenta-list.component.css'] 
          providers: [CuentaService] 
        }) 
        export class CuentaListComponent implements OnInit { 
          constructor() { } 
          ngOnInit() { 
            this.cuentaService.getAll().subscribe( 
              data => { 
                this.cuentas = data; 
              }, 
              error => console.log(error) 
            ) 
          } 
        } 

Modifique la página HTML src/app/cuenta-list/cuenta-list.component.html para mostrar la lista de cuentas.

<h2>Beer List</h2> 
        <div *ngFor="let c of cuentas"> 
          {{c.nombre}} 
        </div> 

Actualice la página HTML app.component.html para tener la cuenta CuentaListComponent con el nombre.

<div style="text-align:center"> 
          <h1> 
            Bienvenido a la aplicación de la cuenta 
          </h1> 
        </div> 
        <app-cuenta-list></app-cuenta-list> 

Ahora la aplicación está completa.

Debe iniciar la parte Spring Boot, que se ejecuta en el puerto 8080, y la parte que sirve al cliente en el puerto 4200.

Puntos clave

  • Angular es simple y accesible para desarrolladores de Java.

  • El enlace Spring y Angular se realiza a través de la API REST.

  • Las aplicaciones SPA (Angular, ReactJS, etc.) han reemplazado masivamente a las aplicaciones JSP y JSF, y Java y Spring se centrarán en el backend.

Introducción Spring-HATEOAS

En este capítulo vamos a abordar los diferentes niveles de madurez de las API REST.

Leonard Richardson definió un modelo de madurez de cuatro niveles: Leonard Richardson

El nivel cero se corresponde con el modelo más sencillo que está en la base de los servicios web de tipo SOAP, a menudo con mensajes en XML. Una URL básica sirve todas las consultas de tipo POST y devuelve OK (código de retorno 200).

POST http://miservidor/aplicacioncentral 

El nivel uno se corresponde con la adición por codificación en la URL del nombre de un recurso y de sus identificadores, lo que añade un poco de semántica.

POST http://miservidor/usuario/55/email 

El nivel dos se corresponde con el uso de otros verbos como GET, PUT, DELETEPOST y PATCH, que indican lo que se quiere hacer con la consulta e introducen las respuestas universalmente reconocidas 1xx, 2xx, 3xx, 4xx y 5xx. Este es el nivel de las API REST tradicionales.

DELETE http://miservidor/usuario/55 

El nivel tres se corresponde con el nivel HATEOAS (Hypermedia As The Engine Of Application State), que significa hipermedia como motor de estado de la aplicación. Este nivel es una opción para las API SOAP o REST. Consiste en establecer un diálogo mediante la adición de enlaces hipermedia en las respuestas a las llamadas de servicio para guiar al usuario en el uso del API.

La respuesta se enriquece con enlaces tales como:

{ 
          "content":"Hola", 
          "_links":{ 
            "self":{  
              "href":"http://localhost:8080/echo?name=Hola" 
            } 
          } 
        } 

Cuando el contenido de la respuesta JSON contiene enlaces hipermedia creados con Spring HATEOAS, también se pueden comprobar los enlaces resultantes: 

mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON)) 
        .andExpect(jsonPath("$.links[?(@.rel == 
        'self')].href").value("http://localhost:8080/people")**); 

Cuando el contenido de la respuesta XML contiene vínculos hipermedia creados con Spring HATEOAS, se pueden comprobar los vínculos resultantes:

Map<String, String> ns = Collections.singletonMap("ns",  
          "http://www.w3.org/2005/Atom"); 
        mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML)) 
          .andExpect(xpath("/person/ns:link[@rel='self']/@href",ns) 
          .string("http://localhost:8080/people")); 

Hay cuatro formatos principales de HATEOAS:

  • JSON-LD: JSON-based serialization for linked data: método que permite codificar datos estructurados en un archivo JSON para especificar un contexto con el objetivo de que tenga sentido.

  • Collection + JSON: creada por Mike Amundsen en 2011. Pretende ser un tipo de hipermedia diseñado para soportar la lectura, escritura y consulta de colecciones sencillas.

  • SIREN: Structured Interface for Representing Entities, super-rad hypermedia

  • HAL: Hypertext Application Language

Spring proporciona un módulo para facilitar el uso de HATEOAS. Este módulo también se puede utilizar en un proyecto Spring Boot basado en Spring MVC para servicios web REST con descripción. HATEOAS es de estilo HAL. Este módulo se encuentra actualmente en la versión 1.12. 11.RELEASE. HAL

Hay que prever una migración para los proyectos que utilizan la versión 1.0.

A continuación, se muestran extractos del ejemplo del capítulo descargable que ilustra cómo utilizar este módulo.

Para usarlo, es suficiente con declarar la dependencia para Maven:

<dependency> 
          <groupId>org.springframework.hateoas</groupId> 
          <artifactId>spring-hateoas</artifactId> 
          <version>1.12.11.RELEASE</version> 
        </dependency> 

HALSpring también proporciona un navegador de API integrado HAL que usamos para el ejemplo, cuyas fuentes están en la dirección https://github.com/spring-projects/spring-data-rest/tree/main/spring-data-rest-hal-explorer

Para usarlo, se debe declarar con la siguiente dependencia:

<dependency> 
          <groupId>org.springframework.data</groupId>  
          <artifactId>spring-data-rest-hal-browser</artifactId> 
          <scope>runtime</scope> 
        </dependency> 

El proyecto HAL también añade enlaces hipermedia para aplicaciones que originalmente no los tienen.

La versión se deduce automáticamente si se trata de un proyecto Spring Boot.

1. Ejemplo de enlaces hipermedia codificados manualmente Enlaces hipermedia

En este ejemplo se muestra cómo codificar un controlador para que exponga información HATEOAS.

Ejemplo completo

@Data 
        @Entity  
        @RestResource 
        @NoArgsConstructor 
        @FieldDefaults(level = AccessLevel.PRIVATE) 
        public class Country { 
         @Id 
         @GeneratedValue 
         Long id; 
         String name; 
        } 
         
        @Data 
        @Entity 
        @NoArgsConstructor 
        @RestResource 
        @FieldDefaults(level = AccessLevel.PRIVATE) 
        public class City { 
         @GeneratedValue 
         @Id Long id; 
         String name; 
         @ManyToOne Country country; 
        } 
         
        public interface CountryRepository extends JpaRepository<Country, Long> { 
        } 
         
        @RepositoryRestResource(path = "metropolises"public interface CityRepository extends JpaRepository<City, Long> \{ 
        }  
         
        @RestController 
        public class GreetingController { 
         private static final String TEMPLATE = "Hello, %s!"; 
         @RequestMapping("/greeting") 
         public HttpEntity<Greeting> greeting( 
         @RequestParam(value = "name", required = false, defaultValue = "World") 
        String name) { 
         
         Greeting greeting = new Greeting(String.format(TEMPLATE, name)); 
         greeting.add(linkTo(methodOn(GreetingController.class).greeting(name) 
        .withSel fRel()); 
         
         return new ResponseEntity<Greeting>(greeting, HttpStatus.OK); 
         } 
        } 

Se crea la entidad y se le agrega un enlace "linkTo". Se trata del enlace que apunta directamente a la entidad en la respuesta:

Greeting greeting = new Greeting(String.format(TEMPLATE, name)); 
        greeting.add(linkTo(methodOn(GreetingController.class). 
        greeting(name).withSel fRel());return new ResponseEntity 
        <Greeting>(greeting, HttpStatus.OK); 

Después de una regeneración y del lanzamiento, podemos acceder a la API a través del HAL browser en http://localhost:8080/browser/index.html#/

images/hal-browser-1.png

En esta pantalla distinguimos dos áreas: la zona izquierda para la navegación y la derecha para responder a la llamada REST. En la parte inferior izquierda, tenemos la sección que se corresponde con los enlaces hipermedia. El botón GET se utiliza para navegar por el verbo GET y el botón NON-GET para el resto de los verbos: POST, PUT y DELETE.

El botón GET abre una ventana de tipo «Expand URI Template» que permite ingresar el JSON como entrada.

El botón NON-GET muestra la ventana Create/Update:

La documentación de referencia se puede encontrar en la dirección: https://docs.spring.io/spring-hateoas/docs/current/

2. Ir más allá con hipermedia

Podemos usar el soporte básico y añadir más controles con el soporte de Spring HATEOAS cuando sea necesario, como parte de un servicio web REST con integración Jackson (para JSON) o JAXB (para XML).

Los ejemplos ilustran el uso de Jackson, pero el equivalente JAXB también funciona perfectamente; cada uno es adecuado en su dominio de uso.

Clases utilizadas:

Clase

Utilidad

Observaciones

Link

LinkDescribe un enlace con los atributos Href y Rel.

Contiene constantes como Link.REL_SELF.

ResourceSupport

ResourceSupportExtiende un POJO para representar el recurso.

Es posible utilizar anotaciones @JsonCreator, @JsonProperty("xxx") en elementos de la clase recurso.

Ejemplo

@RequestMapping(method = RequestMethod.GET, headers = "Accept= 
        application/json, application/xml", produces = { "application/json" }) 
        public HttpEntity<List<Link>> showLinks() {  
          List<Link> links = new ArrayList<Link>(); 
          Link people = linkTo(PersonController.class).withRel("people"); 
          links.add(people); 
          return new HttpEntity<List<Link>>(links); 
        } 

Se crea el enlace y se devuelve en la respuesta.

3. Autoconfiguración por anotaciones

La anotación @EnableHypermediaSupport preconfigura el tipo, pero podemos especificarlo para mayor visibilidad.

@Configuration 
        @EnableEntityLinks 
        @EnableHypermediaSupport(type = 
        EnableHypermediaSupport.HypermediaType.HAL EnableHypermediaSupport.HypermediaType.HAL
        @ComponentScan(basePackageClasses = {ObjectMapperCustomizer.class, 
        ModuleRegistry.class}) 
        public class RestConfiguration extends 
        RepositoryRestConfigurerAdapter {...} 

Estas anotaciones añaden automáticamente la creación de enlaces hipermedia. Especificamos el tipo EnableHypermediaSupport.HypermediaType.HAL, que es el único tipo disponible actualmente.

4. Proveedores de relaciones

Puede usar el comportamiento predeterminado que añade List para designar las colecciones o encontrar una manera de pluralizar los nombres de los recursos. De forma predeterminada, se añade el sufijo List. Algunas veces es preferible poner una «s» para indicar el plural, pero no todas las palabras en plural terminan con «s».

Las clases anotadas @Controller y @ExposedResourceFor utilizan enlaces de forma transparente.

Ejemplo

relProvider.getSingleResourceRelFor(MyController.class) 

Podemos codificar o usar un deflector existente para pluralizar los nombres. Si el deflector EVO (https://github.com/atteo/evo-inflector) está en el classpath, las relaciones se derivan utilizando el algoritmo pluralizing, que es muy potente: evo-inflector

<dependency> 
         <groupId>org.atteo</groupId> 
         <artifactId>evo-inflector</artifactId> 
         <version>1.2.2</version> 
        </dependency> 

Gracias a este deflector, los enlaces en inglés son automáticamente puestos en plural en las respuestas.

5. Proveedor de URI compacto

Existe otro formato reconocido y más compacto para describir las relaciones. Se trata del formato Curie (Compact URI), que consiste en proporcionar un acceso directo. A continuación, se presenta un ejemplo de configuración: Curie

@Configuration 
        @EnableWebMvc 
        @EnableHypermediaSupport(type= {HypermediaType.HAL}) 
        public class Config { 
         @Bean 
         public CurieProvider curieProvider() { 
         return new DefaultCurieProvider("ex", new 
        UriTemplate("http://www.example.com/rels/\{rel}")); 
         } 
        } 

Ejemplo: persons proporciona enlaces http://example.com/rels/persons si ex se define como http://example.com/rels/{rels} en el scope de la consulta:

{ 
         _"links" : { 
         "self" : { href: "http://myhost/person/1" }, 
         "curies" : \{ 
         "name" : "ex", 
         "href" : "http://example.com/rels/{rel}", 
         "templated" : true 
         }, 
         "ex:orders" : \{ href : "http://myhost/person/1/orders" } 
         }, 
         "firstname" : "Dave", 
         "lastname" : "Matthews" 
        } 

6. Soporte del lado del cliente

Spring HATEOAS proporciona una API inspirada en la librería JavaScript Traverson (https://blog.codecentric.de/en/2013/11/traverson/) para navegar con los enlaces hipermedia. Traverson

Ejemplo

Map<String, Object> parameters = new HashMap<>(); 
        parameters.put("user", 27);  
         
        Traverson traverson = new Traverson(new 
        URI("http://localhost:8080/api/"), MediaTypes.HAL_JSON); 
         
        String name = traverson.follow("movies", "movie", "actor"). 
         withTemplateParameters(parameters). 
         toObject("$.name"); 

Ejemplo más complejo:

ParameterizedTypeReference<Resource<Item>> 
        resourceParameterizedTypeReference = new 
        ParameterizedTypeReference<Resource<Item>>() {}; 
        Resource<Item> itemResource = 
        traverson.follow(rel("items").withParameter("projection""noImages")).follow("$._embedded.items[0]._links.self.href"). 
        toObject(resourceParameterizedTypeReference); 

Y:

Resources<Item> itemResource = traverson. 
                follow(rel("items")). 
                toObject(resourceParameterizedTypeReference); 

7. Descubrimiento de enlaces del lado del cliente

Esta prueba verifica que el enlace es correcto:

String content = "{'_links' : { 'foo' : { 'href' : '/foo/bar' }}}"; 
        LinkDiscoverer discoverer = new HalLinkDiscoverer(); 
        Link link = discoverer.findLinkWithRel("foo", content); 
        assertThat(link.getRel(), is("foo")); 
        assertThat(link.getHref(), is("/foo/bar")); LinkDiscoverer 

8. Uso del @RepositoryRestResource

Es posible añadir automáticamente enlaces hipermedia usando anotaciones: @RepositoryRestResource(collectionResourceRel = "regiones", path = "regiones").

Construyamos un ejemplo con comunidades y regiones.

Creamos una clase Comunidad y una clase Region para mostrar un caso de uso para los enlaces HATEOAS.

La entidad JPA Comunidad:

Comunidad.java

@Entity 
        @Table(name = "comunidad"@Getter 
         
        @Setter 
        public class Comunidad implements Serializable { 
         
          private static final long serialVersionUID = 1L; 
         
          @Id 
          @GeneratedValue(strategy = GenerationType.IDENTITY) 
          private Long id; 
         
          @Column(name = "codigo") 
          private String codigo; 
         
          @Column(name = "nombre") 
          private String nombre; 
         
          @ManyToOne 
          private Region region; 
         
          public Comunidad nombre(String nombre) { 
            this.nombre = nombre; 
            return this; 
          } 
         
          public Comunidad region(Region region) { 
            this.region = region; 
            return this; 
          } 
         
          @Override 
          public boolean equals(Object o) { 
            if (this == o) { 
              return true; 
            } 
            if (o == null || getClass() != o.getClass()) { 
              return false; 
            } 
            Comunidad comunidad = (Comunidad) o; 
            if (comunidad.getId() == null || getId() == null) { 
              return false; 
            } 
            return Objects.equals(getId(), comunidad.getId()); 
          } 
         
          @Override 
          public int hashCode() { 
            return Objects.hashCode(getId()); 
          } 
         
        } 

Es necesario codificar la relación de Comunidad - Región:

@ManyToOne 
        private Region region; 

La entidad JPA Region:

Región.java

@Entity 
        @Table(name = "region"@Builder 
        @NoArgsConstructor 
        @Data 
        @ToString 
        @NoArgsConstructor 
        public class Region implements Serializable { 
         
          private static final long serialVersionUID = 1L; 
         
          //Constructor 
          public Region(Long id, String codigo, String nombre, Set<Comunidad> 
        comunidades) { 
            this.id = id; 
            this.codigo = codigo; 
            this.nombre = nombre; 
            this.comunidades = comunidades; 
          } 
         
          @Id 
          @GeneratedValue(strategy = GenerationType.IDENTITY) 
          private Long id; 
         
          @Column(name = "codigo") 
          private String codigo; 
         
          @Column(name = "nombre") 
          private String nombre; 
         
          @OneToMany(mappedBy = "region") 
          @JsonIgnore 
          private Set<Comunidad> comunidades = new HashSet<>(); 
         
          public Region codigo(String codigo) { 
            this.codigo = codigo; 
            return this; 
          } 
         
          public Region nombre(String nombre) { 
            this.nom = nombre; 
            return this; 
          } 
         
          public Set<Comunidad> getComunidades() { 
            return comunidades; 
          } 
         
          public Region comunidades(Set<Comunidad> comunidades) { 
            this.comunidades = comunidades; 
            return this; 
          } 
         
          public Region addComunidad(Comunidad comunidad) { 
            this.comunidades.add(comunidad); 
            comunidad.setRegion(this); 
            return this; 
          } 
         
          public Region removeComunidad(Comunidad comunidad) { 
            this.comunidades.remove(comunidad); 
            comunidad.setRegion(null); 
            return this; 
          } 
         
          public void setComunidades(Set<Comunidad> comunidades) { 
            this.comunidades = comunidades; 
          } 
         
          @Override 
          public boolean equals(Object o) { 
            if (this == o) { 
              return true; 
            } 
            if (o == null || getClass() != o.getClass()) { 
              return false; 
            } 
            Region region = (Region) o; 
            if (region.getId() == null || getId() == null) { 
              return false; 
            } 
            return Objects.equals(getId(), region.getId()); 
          } 
         
          @Override 
          public int hashCode() { 
            return Objects.hashCode(getId()); 
          } 
        } 

Es necesario codificar la relación Región - Comunidad. Codificamos la relación OneToMany de Region a Comunidad. De hecho, una región corresponde a una lista de comunidades.

@OneToMany(mappedBy = "region"@JsonIgnore 
        private Set<Comunidad> comunidades = new HashSet<>(); 

El repository de Comunidad:

ComunidadRepository.java

@RepositoryRestResource(collectionResourceRel = "comunidades", path = 
        "comunidades"public interface ComunidadRepository extends 
        JpaRepository<Comunidad, Long> { 
        } 

El repository de Region:

RegionRepository.java

@RepositoryRestResource(collectionResourceRel = "Regiones", path = 
        "regiones"public interface ComunidadRepository extends JpaRepositoryRegion, Long> { 
        } 

El lanzador y su runner:

Hateoasex1Application.java

@SpringBootApplication 
        public class Hateoasex1Application { 
            public static void main(String[] args) { 
                SpringApplication.run(Hateoasex1Application.class, args); 
            } 
        } 

El runner llena una base de datos en la memoria:

Hateoasex1Application.java

@Component 
        public class Ex1CommandLineRunner implements  
        org.springframework.boot.CommandLineRunner { 
          private static final Logger LOGGER = 
        LoggerFactory.getLogger(Ex1CommandLineRunner.class); 
         
          @Autowired 
          private RegionRepository regionRepository; 
          @Autowired 
          private ComunidadRepository comunidadRepository; 
         
            @Override 
          public void run(String... strings) throws Exception { 
            Region r53=new Region().codigo("53").nombre("Cantábrico"); 
            Comunidad d22=new Comunidad().codigo("22").nombre("").region(r53); 
            Comunidad d29=new Comunidad().codigo("29").nombre("País Vasco") 
        .region(r53); 
            Comunidad d35=new 
        Comunidad().codigo("35").nombre("Cantabria").region(r53); 
            Comunidad d56=new Comunidad().codigo("56").nombre("Asturias") 
        .region(r53); 
         
            Region r52=new Region().codigo("52").nombre("Mediterráneo"); 
            Comunidad d44=new 
        Comunidad().codigo("44").nombre("Cataluña").region(r52); 
            Comunidad d49=new Comunidad().codigo("49").nombre("Comunidad 
        Valenciana").region(r52); 
            Comunidad d53=new 
        Comunidad().codigo("53").nombre("Andalucía").region(r52); 
            Comunidad d72=new Comunidad().codigo("72").nombre("Murcia") 
        .region(r52); 
            Comunidad d85=new 
        Comunidad().codigo("85").nombre("Melilla").region(r52); 
         
            regionRepository.save(r53); 
            regionRepository.save(r52); 
            comunidadRepository.save(d22); 
            comunidadRepository.save(d29); 
            comunidadRepository.save(d35); 
            comunidadRepository.save(d56); 
            comunidadRepository.save(d44); 
            comunidadRepository.save(d49); 
            comunidadRepository.save(d53); 
            comunidadRepository.save(d72); 
            comunidadRepository.save(d85); 
         
            regionRepository.findAll().forEach(System.out::println); 
            comunidadRepository.findAll().forEach(System.out::println); 
          } 
        } 

La base de datos se configura en el archivo de configuración application.properties.

application.properties:

spring.h2.console.enabled=true 
        spring.h2.console.path=/h2_console 
        spring.datasource.url=jdbc:h2:mem:testdbdep2 
        spring.datasource.username=sa 
        spring.datasource.password= 
        spring.datasource.driverClassName=org.h2.Driver 
        spring.jpa.hibernate.ddl-auto=update 
        spring.jpa.show-sql=true 

Cuando lanzamos la aplicación, tenemos:

En: http://localhost:8080/

Vemos esto en la vista JSON decodificada:

_links 
          comunidades 
            href    "http://localhost:8080/comunidades{?page,size,sort}" 
            templated   true 
          regiones 
            href    "http://localhost:8080/regiones{?page,size,sort}" 
            templated   true 
          profile 
            href    "http://localhost:8080/profile" 

Si mostramos en un navegador el contenido JSON devuelto por el servicio, vemos los enlaces que se muestran. Estos enlaces son funcionales. Es posible navegar de una entidad a otra haciendo clic en los enlaces.

En raw data esto da:

{ 
          "_links" : { 
            "comunidades" : { 
              "href" : "http://localhost:8080/comunidades{?page,size,sort}", 
              "templated" : true 
            }, 
            "regiones" : { 
              "href" : "http://localhost:8080/regiones{?page,size,sort}", 
              "templated" : true 
            }, 
            "profile" : { 
              "href" : "http://localhost:8080/profile" 
            } 
          } 
        } 

Si elegimos localhost:8080/regiones, entonces tenemos la lista que aparece:

{ 
          "_embedded" : { 
            "regiones" : [ { 
              "code" : "53", 
              "nombre" : "Cantábrico", 
              "_links" : { 
                "self" : { 
                  "href" : "http://localhost:8080/regiones/1" 
                }, 
                "region" : { 
                  "href" : "http://localhost:8080/regiones/1" 
                }, 
                "comunidades" : { 
                  "href" : "http://localhost:8080/regiones/1/comunidades" 
                } 
              } 
            }, { 
              "code" : "52", 
              "nom" : " Mediterráneo", 
              "_links" : { 
                "self" : { 
                  "href" : "http://localhost:8080/regiones/2" 
                }, 
                "region" : { 
                  "href" : "http://localhost:8080/regiones/2" 
                }, 
                "comunidades" : { 
                  "href" : "http://localhost:8080/regiones/2/comunidades" 
                } 
              } 
            } ] 
          }, 
          "_links" : { 
            "self" : { 
              "href" : "http://localhost:8080/regiones{?page,size,sort}", 
              "templated" : true 
            }, 
            "profile" : { 
              "href" : "http://localhost:8080/profile/regiones" 
            } 
          }, 
          "page" : { 
            "size" : 20, 
            "totalElements" : 2, 
            "totalPages" : 1, 
            "number" : 0 
          } 
        } 

y todos los enlaces son funcionales.

VM6883:64

Puntos clave

  • Spring ofrece un buen soporte para la hipermedia HATEOAS.

  • Los enlaces hipermedia permiten navegar a través de las respuestas.

  • HATEOAS puede ser un plus en un proyecto, ya que ayuda a ver los enlaces.

Introducción REST Docs

Las aplicaciones se abren e interconectan cada vez más. Exponen y comparten sus estados a través de interfaces que, hoy en día, a menudo tienen forma de API REST con microservicios. Estas interfaces deben estar documentadas para facilitar el acceso a nuestras aplicaciones.

La documentación de las API públicas de LinkedIn, Facebook, Twitter, Google está particularmente bien hecha y algunas veces se compara con la documentación de nuestras API internamente, lo que nos impulsa a dedicar una cantidad considerable de tiempo para mantener la documentación clara, precisa y actualizada.

Para responder a esta necesidad, Spring ofrece una librería Spring REST Docs que se puede utilizar para crear documentación automática para los servicios REST. Esta posibilidad complementa a herramientas como Swagger (estática y dinámica) con SpringFox y el HAL Browser que, por ejemplo, muestran cómo funcionan los servicios. HAL Browser

La librería se basa en Asciidoctor y extractos de documentación para generar automáticamente, durante la compilación, texto plano o HTML.

Asciidoctor es muy potente para crear documentación.

También es posible generar Markdown (archivo *.md), que es muy práctico para usar en GitLab o GitHub, por ejemplo. La librería se utiliza junto con librerías de pruebas como jUnit y TestNG. La librería utiliza las pruebas para generar la documentación de extracción para las consultas y respuestas. Documenta las API basándose en la ejecución de pruebas, lo que garantiza que la API esté actualizada y que los ejemplos que muestra funcionen. jUnit TestNG

La documentación se centra en los recursos del servicio REST y detalla, por un lado, las consultas HTTP que consume la API y, por otro lado, las respuestas que produce, manteniendo oculta la parte de implementación.

En las siguientes secciones usaremos Maven, pero también es posible hacer el equivalente con Gradle. Del mismo modo, solo veremos parcialmente la documentación generada, ya que es muy completa. El ejemplo del capítulo permite ver rápidamente la documentación generada.

La librería de generación de documentos se inyecta en las pruebas. Podemos obtener resultados significativamente diferentes en función de las librerías de pruebas utilizadas. Del mismo modo, hay varios niveles de codificación con el uso de anotaciones específicas, como las anotaciones HATEOAS, que también hacen contribuciones adicionales a la documentación. HATEOAS

La documentación se genera a través de plantillas de documentación que se completan con los resultados de las pruebas:

images/13EP01N.png

El contenido de la documentación se puede volver a inyectar de nuevo en las fuentes para que sea visible desde la raíz del servidor HTTP. Entonces, la documentación forma parte de la aplicación. Es decir, viene con la aplicación y se puede acceder desde una API REST, como para Swagger. La documentación sigue las versiones de la aplicación entregada y siempre está actualizada.

images/cap13_pag3.png

Los siguientes ejemplos utilizan Spring Boot.

La documentación se genera en la parte estática del contenido. De forma predeterminada, Spring Boot expone los recursos estáticos en el directorio /static o /public o /resources o /META-INF/resources, que se encuentra en el classpath o en la raíz de ServletContext. De hecho, este último devuelve una URL válida hacia los recursos estáticos desde la versión 1.4.5 de Spring Boot. La prueba utiliza el ResourceHttpRequestHandler de Spring MVC, que se puede modificar con la adición de nuestro propio WebMvcConfigurerAdapter, sobrecargando el método addResourceHandlers.

La ubicación de la documentación, que se inyecta automáticamente, se puede redefinir a través del siguiente argumento de configuración:

spring.mvc.static-path-pattern=/resources/** 

La generación de la documentación se realiza antes del packaging del jar que se va a integrar.

images/13EP03N.png

1. Dependencia de la librería de pruebas

Para configurar el proyecto en Maven, debemos agregar estas dependencias:

Dependencia de restdocs mockmvcs (el framework de mock de pruebas Spring MVC):

<dependencies> 
          <dependency> 
            <groupId>org.springframework.restdocs</groupId> 
            <artifactId>spring-restdocs-mockmvc</artifactId> 
            <version>2.0.46.RELEASE</version> 
          </dependency> 
        </dependencies> 

2. Dependencia de los plugins Maven

Lista de plugins:

Identificación

Utilidad

generate-docs

Generación de la documentación.

copy-resources

Retroinyección de la documentación en las fuentes como recursos estáticos en la aplicación Spring Boot.

A continuación, se muestra el contenido del archivo pom.xml, cuyo código fuente completo está en el ejemplo descargable.

He aquí está el POM en formato yaml para mayor visibilidad.

Si queremos, podemos utilizar un formato distinto a XML para POM gracias a la extensión Polyglot de Maven Polyglot:

Lenguaje

ArtifactId

Accepted POM files

Atom

polyglot-atom

pom.atom

Clojure

polyglot-clojure

pom.clj

Groovy

polyglot-groovy

pom.groovy, pom.gy

Java

polyglot-java

pom.java

Kotlin

polyglot-kotlin

pom.kts

Ruby

polyglot-ruby

pom.rb, Mavenfile, Jarfile, Gemfile

Scala

polyglot-scala

pom.scala

XML

polyglot-xml

pom.xml

YAML

polyglot-yaml

pom.yaml, pom.yml

El archivo XML se puede convertir a YAML con el comando:

mvn io.takari.polyglot:polyglot-translate-plugin:translate 
        Dinput=pom.xml -Doutput=pom.yml 

build: 
         plugins: 
          plugin: 
           - 
           groupId: "org.springframework.boot" 
            artifactId: "spring-boot-maven-plugin" 
           - 
           groupId: "org.apache.maven.plugins" 
            artifactId: "maven-surefire-plugin" 
            configuration: 
             includes: 
              include: "**/*Documentation.java" 
           - 
           groupId: "org.asciidoctor" 
            artifactId: "asciidoctor-maven-plugin" 
            version: "1.5.3" 
            executions: 
             execution: 
              id: "generate-docs" 
              phase: "prepare-package" 
              goals: 
               goal: "process-asciidoc" 
              configuration: 
               backend: html 
               doctype: book 
            dependencies: 
             dependency: 
              groupId: "org.springframework.restdocs" 
              artifactId: "spring-restdocs-asciidoctor" 
              version: "${spring-restdocs.version}" 
           - 
           artifactId: "maven-resources-plugin" 
            executions: 
             execution: 
              id: "copy-resources" 
              phase: "prepare-package" 
              goals: 
               goal: "copy-resources" 
              configuration: 
               outputDirectory: "${project.build.outputDirectory}/static/ 
        docs" 
               resources: 
                resource: 
                 directory: "${project.build.directory}/generated-docs" 

Las dependencias son numerosas.

A continuación, generamos la documentación durante la fase prepare-package ejecutando el siguiente comando Maven:

mvn prepare-package 

3. Los extractos (snippets)

La documentación se basa en extractos, que son plantillas de documentos que se valorizan por medio de las consultas y respuestas generadas por las pruebas Spring MVC, Spring WebFlux o REST Assured (http://rest-assured.io/).

Los extractos de referencia se generan de forma predeterminada durante la fase de compilación y personalizaremos estos tramos para nuestros propios extractos.

Veremos cómo hacer esto con los frameworks JUnit y MockMvc. Es posible utilizar otras librerías, como TestNG, pero nos limitaremos a jUnit. En los ejemplos del libro, hay disponible un ejemplo con WebTestClient.

VM6883:64

Ejemplo JUnit 5 (Jupiter)

La documentación del ejemplo se genera en el directorio target/generated-snippets. Sería posible especificar otro directorio.

Código de mockMvc

El mock es el que utiliza Spring REST Docs para generar la documentación. Es relativamente complejo. Lo ideal es usar el ejemplo que está disponible para su descarga y seguir las instrucciones.

private MockMvc mockMvc; 
         
        @ExtendWith(RestDocumentationExtension.class) 
          public class JUnit5ExampleTests { 
         
          private MockMvc mockMvc; 
         
          @BeforeEach 
          public void setUp(WebApplicationContext webApplicationContext, 
          RestDocumentationContextProvider restDocumentation) { 
            this.mockMvc = 
               MockMvcBuilders.webAppContextSetup(webApplicationContext) 
                 .apply(documentationConfiguration(restDocumentation)) 
                 .build(); 
        } 

Llamada de servicio:

this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) 
          .andExpect(status().isOk()) 
          .andDo(document("index")); 

Durante las builds, la herramienta genera extractos que son plantillas reutilizables. A continuación, podemos personalizar estos extractos:

  • <output-directory>/index/curl-request.adoc

  • <output-directory>/index/http-request.adoc

  • <output-directory>/index/http-response.adoc

  • <output-directory>/index/httpe-request.adoc

  • <output-directory>/index/request-description.adoc

  • <output-directory>/index/response-description.adoc

Estos archivos se utilizan como plantilla y se asocian con las plantillas incluidas en src/main/asciidoc, como por ejemplo:

  • Guía-de-inicio.adoc

  • Guía-de-apis.adoc

Podemos personalizar el número y los nombres de los archivos mediante la composición de documentos a partir de extractos usando macros de inclusión Asciidoctor.

Por ejemplo: include::{snippets}/index/curl-request.adoc[]

VM6883:64

Consulta y respuesta

En el archivo request-description.adoc, se genera el cuerpo de la consulta predeterminada.

El de la respuesta, en response-description.adoc.

Para un Body:

{ 
          "contact": { 
            "name": "Jane Doe", 
            "email": "jane.doe@example.com" 
          } 
        } 

Lo podemos configurar de la siguiente manera:

this.mockMvc.perform(get("/user/5") 
        .accept(MediaType.APPLICATION_JSON)) 
          .andExpect(status().isOk()) 
          .andDo(document("index", 
          responseFields( 
            fieldWithPath("contact.email") 
            .description("The user's email address"), [...] 

La consulta se presenta en forma de tabla, que tiene como extracto el archivo request-fields.adoc.

También podemos usar rutas JSON.

Asimismo, es posible especificar el tipo del campo a través del tipo (Object) de la clase FieldDescriptor.

.andDo(document("index", 
          responseFields( 
          fieldWithPath("contact.email").type(JsonFieldType.STRING) 
            .description("The user's email address")))); 

Respuesta con JSON anidado

Podemos tener una respuesta con un JSON anidado.

Para una descripción como esta:

{ 
          "weather": { + 
            "wind": { + 
            "speed": 15.3, + 
            "direction": 287.0 + 
           }, 
          "temperature": { 
            "high": 21.2, 
            "low": 14.8 
            } 
          } 
        } 

Podemos tener este código:

this.mockMvc.perform(get("/locations/1").accept(MediaType. 
        APPLICATION_JSON)) + 
        .andExpect(status().isOk()).andDo(document("location", + 
        responseBody(beneathPath("weather.temperature")))); 
         
        Identificador : beneath-$\{path}. 
         
        Snippet : response-description-beneath-weather.temperature.adoc 

Código:

responseBody(beneathPath("weather.temperature") 
        .withSubsectionId("temp")); 

Documentación de los campos:

this.mockMvc.perform(get("/locations/1").accept(MediaType. 
        APPLICATION_JSON)) + 
        .andExpect(status().isOk()) + 
        .andDo(document("location", + 
        responseFields(beneathPath("weather.temperature"), + 
        fieldWithPath("high").description( + 
        "The forecast high in degrees celcius"), + 
        fieldWithPath("low") + 
        .description("The forecast low in degrees celcius")))); 

1. Parámetros de consulta

Utilizamos el método requestParameters:

this.mockMvc.perform(get("/users?page=2&per_page=100")) + 
        .andExpect(status().isOk()) + 
        .andDo(document("users", requestParameters( + 
        parameterWithName("page").description("The page to retrieve"), + 
        parameterWithName("per_page").description("Entries per page") + 
        ))); 

o:

this.mockMvc.perform(post("/users").param("username", "Tester")) + 
        .andExpect(status().isCreated()) + 
        .andDo(document("create-user", requestParameters( + 
        parameterWithName("username").description("The user's username") + 
        ))); 

2. Los parámetros incluidos en el path

Usamos el método pathParameters:

this.mockMvc.perform(get("/locations/\{latitude}/\{longitude}"51.5072, 0.1275)) + 
        .andExpect(status().isOk()) + 
        .andDo(document("locations", pathParameters( + 
        parameterWithName("latitude").description("The location's latitude"), + 
        parameterWithName("longitude").description("The location's longitude") + 
        ))); 

3. Las Request parts

Utilizamos el método requestParts:

this.mockMvc.perform(multipart("/upload").file("file""example".getBytes())) 
        .andExpect(status().isOk()) 
        .andDo(document("upload", requestParts( 
        partWithName("file").description("The file to upload")) 
        )); 

4. Las Request parts payloads

Prueba con un mock para probar varias imágenes como archivos adjuntos.

MockMultipartFile image = new MockMultipartFile("image", "image.png""image/png", + 
        "<<png data>>".getBytes()); + 
        MockMultipartFile metadata = new MockMultipartFile("metadata", "", + 
        "application/json", "\{ \"version\": \"1.0\"}".getBytes()); + 
        + 
        this.mockMvc.perform(fileUpload("/images").file(image).file(metadata) + 
        .accept(MediaType.APPLICATION_JSON)) + 
        .andExpect(status().isOk()) + 
        .andDo(document("image-upload", requestPartBody("metadata"))); 

5. Los campos

Prueba de metadatos con un mock para probar con varias imágenes como archivos adjuntos.

MockMultipartFile image = new MockMultipartFile("image", "image.png""image/png", + 
        "<<png data>>".getBytes()); +  
        MockMultipartFile metadata = new MockMultipartFile("metadata", "", + 
        "application/json", "\{ \"version\": \"1.0\"}".getBytes()); + 
        + 
        this.mockMvc.perform(fileUpload("/images").file(image) 
        .file(metadata) + 
        .accept(MediaType.APPLICATION_JSON)) + 
        .andExpect(status().isOk()) + 
        .andDo(document("image-upload", requestPartFields("metadata", + 
        fieldWithPath("version").description("The version of the image")))); 

6. Enlaces hipermedia en la respuesta

Como vimos en el capítulo sobre documentación hipermedia de los servicios REST, es posible ofrecer un recorrido por la API a través de las respuestas.

Los formatos de enlace Atom y HAL se tienen en cuenta correctamente. Atom HAL

Es posible documentar estos enlaces hipermedia especificando una descripción para cada enlace:

this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) 
        .andExpect(status().isOk()) 
        .andDo(document("index", links( 
        linkWithRel("alpha").description("Link to the alpha resource"),  
        linkWithRel("bravo").description("Link to the bravo resource")))); 

También es posible ignorar los enlaces self y curies:

public static LinksSnippet links(LinkDescriptor... descriptors) \{ 
        return 
        HypermediaDocumentation.links(linkWithRel("self").ignored() 
        .optional(), 
        linkWithRel("curies").ignored()).and(descriptors); + 
        } 

A continuación, se muestra un extracto links.adoc que contiene la documentación que se puede incluir.

Descripción que documentará los enlaces en la documentación:

Relación

Descripción

Ciudades

El recurso ciudad

información

El recurso información

Perfil

El perfil ALPS (Application-Level Profile Semantics) del servicio

7. Los encabezados

Los encabezados de la consulta y la respuesta se documentan mediante los métodos requestHeaders y responseHeaders:

this.mockMvc + 
        .perform(get("/people").header("Authorization", "Basic 
        dXNlcjpzZWNyZXQ=")) 
        .andExpect(status().isOk()) 
        .andDo(document("headers",  
        requestHeaders( 
        headerWithName("Authorization").description( 
        "Basic auth credentials")),  
        responseHeaders( 
        headerWithName("X-RateLimit-Limit").description( 
        "The total number of requests permitted per period"), 
        headerWithName("X-RateLimit-Remaining").description( 
        "Remaining requests permitted in current period"), 
        headerWithName("X-RateLimit-Reset").description( 
        "Time at which the rate limit period will reset")))); 
VM6883:64

Personalización de la documentación

Es posible tener en cuenta las restricciones de datos (validadores) para completar la documentación y personalizar las consultas y las respuestas para que coincidan exactamente con la API. Hay un ejemplo entre los ejemplos descargables que muestra estas posibilidades.

Uso de @AutoConfigureRestDocs

Esta anotación se puede aplicar a una clase de prueba para habilitar y configurar la generación automática de documentos Spring REST. Permite configurar el directorio de salida, el esquema y el puerto de los URI generados. Cuando se requiere una configuración adicional, se puede usar un bean RestDocsMockMvcConfigurationCustomizer. RestDocsMockMvcConfigurationCustomizer

VM6883:64

Acoplamiento Swagger 2 Swagger

Vimos que la documentación estática era necesaria y fácil de hacer con Spring y que podíamos usar el navegador HAL para exponer las API. También es posible utilizar Swagger 2. Esta sección describe dos usos de Swagger 2 en proyectos de Spring: uso estándar con Swagger 2 y un uso más avanzado con SpringFox. SpringFox

Swagger permite una generación estática en tiempo de compilación y SpringFox permite una generación dinámica en tiempo de ejecución. Algunas veces usamos ambos juntos.

1. Utilizar Springfox

Como alternativa a la solución estudiada, vamos s ver cómo utilizar la implementación Springfox en nuestro proyecto de ejemplo.

http://springfox.github.io/springfox/docs/current/

El proyecto Springfox automatiza la documentación de las API JSON para las API creadas con Spring.

Dependencia Maven:

<dependency> 
                 <groupId>io.springfox</groupId> 
                 <artifactId>springfox-swagger2</artifactId> 
                 <version>3.0.0</version> 
                </dependency> 
                <dependency> 
                 <groupId>io.springfox</groupId> 
                 <artifactId>springfox-swagger-ui</artifactId> 
                 <version>3.0.0</version> 
                </dependency> 

Esta clase de configuración se utiliza para configurar Springfox:

@Configuration 
                @EnableSwagger2 
                public class SwaggerConfig \{ 
                 @Bean 
                 public Docket api() \{ 
                 return new Docket(DocumentationType.SWAGGER_2) 
                 .select() 
                 .apis(RequestHandlerSelectors.any()) 
                 .paths(PathSelectors.any()) 
                 .build(); 
                 } 
                } 

2. Fuera de Spring Boot

También podemos usar Springfox sin la configuración automática de Spring Boot. En este caso, debe agregar un Handler:

@Override 
                public void addResourceHandlers(ResourceHandlerRegistry registry) { 
                 registry.addResourceHandler("swagger-ui.html") 
                 .addResourceLocations("classpath:/META-INF/resources/"); 
                 registry.addResourceHandler("/webjars/**")  
                 .addResourceLocations("classpath:/META-INF/resources/webjars/"); 
                } 

Acceso a la documentación:

http://localhost:8080/spring-security-rest/api/swagger-ui.html 

Filtrar las API:

@Bean 
                public Docket api() \{ 
                 return new Docket(DocumentationType.SWAGGER_2) 
                 .select() 
                 .apis(RequestHandlerSelectors.basePackage("fr.eni.web.controller")) 
                 .paths(PathSelectors.ant("/foos/*")) 
                 .build(); 
                } 

Información personalizada:

@Bean 
                public Docket api() \{ 
                 return new Docket(DocumentationType.SWAGGER_2) 
                 .select() 
                 .apis(RequestHandlerSelectors.basePackage("com.example.controller")) 
                 .paths(PathSelectors.ant("/foos/*")) 
                 .build() 
                 .apiInfo(apiInfo()); 
                } 
                 
                private ApiInfo apiInfo() \{ 
                 return new ApiInfo( 
                 "My REST API", 
                 "Some custom description of API.", 
                 "API TOS", 
                 "Terms of service", 
                 new Contact("John Doe", "www.example.com", "myeaddress@company.com"), 
                 "License of API", "API license URL", Collections.emptyList()); 
                } 

Mensaje de respuesta de los métodos personalizados:

.useDefaultResponseMessages(false.globalResponseMessage(RequestMethod.GET, 
                 newArrayList(new ResponseMessageBuilder() 
                 .code(500) 
                 .message("500 message") 
                 .responseModel(new ModelRef("Error")) 
                 .build(), 
                 new ResponseMessageBuilder() 
                 .code(403) 
                 .message("Forbidden!") 
                 .build())); 
VM6883:64

Uso con Spring Data Rest Spring Data Rest

Podemos usar Springfox con Spring Data Rest. Es suficiente con importar los modelos a través de la anotación @Import. En los ejemplos descargables, puede consultar el código completo, que resulta demasiado extenso para reproducirlo en su totalidad aquí.

Configuración de Java:

@Import(\{ ... 
                springfox.documentation.spring.data.rest.configuration.SpringDataRest 
                Configuration.class, 
                ...}) 
                 
                @Import(\{ ... 
                springfox.bean.validators.configuration.BeanValidatorPlugins 
                Configuration.class, 
                ...}) 

Por lo tanto, el uso es sencillo y es posible personalizar muchos elementos en la página swagger de la documentación de la aplicación.

VM6883:64

Resumen de la documentación generada

Spring REST Docs se puede utilizar para generar documentación estática para un proyecto. Permite tener una documentación actualizada. Adicionalmente, también se puede utilizar Asciidoctor para generar otras partes de la documentación o para compartir elementos entre varias documentaciones, como documentación de las API, documentación del proyecto o manual de usuario.

VM6883:64

Puntos clave

  • Spring REST Docs se puede utilizar para generar la documentación estática de un proyecto.

  • Adicionalmente, también se puede utilizar Asciidoctor para generar otras partes de la documentación o para compartir elementos entre varias documentaciones, como documentación de las API, documentación del proyecto o manual de usuario.

  • Con estas herramientas, es posible tener documentación de estilo profesional sin dedicarle demasiado tiempo.

VM6883:64

Introducción Spring Boot

En los últimos años, se han realizado muchas inversiones en Spring Boot para conseguir una gran sencillez en el uso del ecosistema basado en el framework Spring.

Principalmente, Spring boot es una clase SpringApplication que proporciona una forma práctica de comenzar una aplicación Spring que arranca con un método main(). En muchas situaciones, podemos delegar al método estático SpringApplication.run() la tarea de iniciar la aplicación. También es un mecanismo de autoconfiguración basado en las dependencias que están relacionadas con la aplicación. Esta aplicación puede ser standalone, un servidor, un Batch, etc. Autoconfiguración

Spring Boot es una aplicación genérica que facilita la creación de aplicaciones estándares. Podemos diseñar rápidamente aplicaciones web, batch o microservicios, basándonos en implementaciones predeterminadas que se pueden personalizar. Para aplicaciones web/web services, Spring permite, por ejemplo, crear aplicaciones independientes autónomas que pueden incorporar un contenedor Tomcat, y es posible elegir Jetty o Undertow o cualquiera de las otras implementaciones disponibles como sustituto para el contenedor de servlets interno o elegir usar un contenedor externo. Tomcat Jetty Undertow

Existe un sistema de autoconfiguración que detecta automáticamente los componentes técnicos que necesita la aplicación. Por lo tanto, es posible codificar módulos autoconfigurables.

La escritura de archivos de descripción de build Gradle o Maven se simplifica utilizando starters, que son módulos autoconfigurables que administran dependencias y sus versiones. La configuración automática tiene en cuenta todos los elementos disponibles. Por ejemplo, Spring Boot examina las librerías que están en CLASSPATH y deduce la herramienta o librería correspondiente, rellenando los argumentos de configuración que faltan por sí solo.

Todo está previsto para poder desplegar rápidamente una aplicación en producción, con instrumentación para la explotación. Todo es sobrecargable, configurable y personalizable.

Es posible prescindir por completo de la configuración mediante archivos XML, lo que generalmente es preferible con la configuración de anotación.

Observe que Spring Boot extiende Spring y Spring Cloud extiende Spring Boot. Por lo tanto, Spring Boot se ha mejorado para servir como base para Spring Cloud. Es decir: Spring Boot y Spring Cloud están vinculados.

Spring Boot 2.6.3 requiere Java 8 y, al menos, la versión 5.x del framework Spring. Las herramientas de build son Maven 3.2+ y Gradle 4+.

En este capítulo se describen en paralelo las versiones 1.5.x y 2.6.x de Spring Boot porque no todos los usuarios han migrado todavía a Spring Boot 2. Concluirá con una guía de migración de la versión 1.5 a la 2.6. En primer lugar, el capítulo describe el uso más común con Spring MVC y, posteriormente, los usos más exóticos. No hay que retrasar la migración a Spring Boot 2 porque la versión 3 se lanzará en breve.

VM6883:64

Configuración de los ejemplos

Describiremos algunos ejemplos de uso.

1. Configuración de Maven para la versión 1.5 de Spring Boot

<parent> 
                 <groupId>org.springframework.boot</groupId> 
                 <artifactId>spring-boot-starter-parent</artifactId> 
                 <version>1.5.22.RELEASE</version> 
                </parent> 
                <dependencies> 
                 <dependency> 
                 <groupId>org.springframework.boot</groupId> 
                 <artifactId>spring-boot-starter-web</artifactId> 
                 </dependency> 
                </dependencies> 

2. Configuración Maven para la versión 2 de Spring Boot

<parent> 
                 <groupId>org.springframework.boot</groupId> 
                 <artifactId>spring-boot-starter-parent</artifactId> 
                 <version>2.6.6.RELEASE</version> 
                </parent> 
                <dependencies> 
                 <dependency> 
                 <groupId>org.springframework.boot</groupId> 
                 <artifactId>spring-boot-starter-web</artifactId> 
                 </dependency> 
                </dependencies> 
                [...] 
                 
                <build> 
                  <plugins> 
                    <plugin> 
                      <groupId>org.springframework.boot</groupId> 
                      <artifactId>spring-boot-maven-plugin</artifactId> 
                    </plugin> 
                  </plugins> 
                </build> 

Para Spring Boot 2, debe utilizar Java 8:

<propiedades> 
                 <java.version>1.8</java.version> 
                </properties> 

3. Uso del hot swapping

El hot swapping permite una recarga rápida en caso de modificaciones de código en los IDE. Si el código se modifica, la aplicación se vuelve a compilar y se recarga automáticamente.

<dependency> 
                  <groupId>org.springframework.boot</groupId> 
                  <artifactId>spring-boot-devtools</artifactId> 
                  <optional>true</optional> 
                </dependency> 

4. Packaging y lanzamiento de la aplicación

Es posible empaquetar la aplicación como un war para desplegarlo en un contenedor o como un jar autónomo.

Para generar un jar:

mvn package 

Y lanzar la aplicación con el jar:

jar tvf target/myproject-0.0.1-SNAPSHOT.jar 

5. Aplicación Spring MVC mínima

El código fuente de este ejemplo está disponible en los archivos de descarga.

Clase SampleController:

package ex1mongodb; 
                import org.springframework.boot.*; 
                import org.springframework.boot.autoconfigure.*; 
                import org.springframework.stereotype.*; 
                import org.springframework.web.bind.annotation.*; 
                 
                @Controller 
                @EnableAutoConfiguration 
                public class SampleController { 
                 @RequestMapping("/") 
                 @ResponseBody 
                 String home() { 
                 return "Hello World!"; 
                 } 
                 public static void main(String[] args) throws Exception { 
                 SpringApplication.run(SampleController.class, args); 
                 } 
                } 

Todo el ecosistema MVC de Spring presentado anteriormente con Spring sin Spring Boot sigue siendo válido. Con Spring Boot, la configuración se simplifica. Por lo tanto, el comportamiento de las anotaciones @RestController y @RequestMapping es idéntico al de una aplicación autónoma sin los starters de Spring Boot. La anotación @EnableAutoConfiguration es particular y soporta la parte de configuración automática que veremos en la siguiente sección.

VM6883:64

Configuración automática Spring Boot Autoconfiguración

La anotación @EnableAutoConfiguration es una composición de anotaciones @Configuration, un juego de anotaciones @Conditional que se configura en función de las anotaciones @ConditionalOnClass y @ConditionalOnMissing y que tiene en cuenta las clases encontradas en el classpath.

El servicio discovery permite cargar en runtime implementaciones de un servicio utilizando las factories. Estas últimas se cargan a través de la clase SpringFactoryLoader, que recupera una lista de factories por el nombre o tipo de la clase.

Spring Boot detecta la presencia del archivo META-INF/spring.factories, que contiene la clave: spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration= 
                fr.eni.spring5.autoconfigure.LibXAutoConfiguration, 
                fr.eni.spring5.autoconfigure.LibXWebAutoConfiguration 

Este archivo tiene su correspondencia en Spring, que contiene más de cien líneas de configuración automática: spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories. La factory tiene más de cien líneas, que contienen los mismos tipos de claves que la mencionada anteriormente.

Configuración automática de los Beans Spring

La configuración automática de Spring Boot intenta configurar la aplicación por sí sola, a partir de las dependencias de los jars añadidos. Por ejemplo, si H2 está en el classpath y no hemos configurado ningún Bean de conexión a una base de datos, entonces una conexión en modo H2 en memoria es autoconfigurable. Esto es práctico, pero hay que ver lo que Spring Boot deduce de sí mismo lanzando la aplicación con la opción -debug, que traza las acciones de configuración automática.

En el archivo application.properties:

debug=true 

En el archivo application.yml:

debug: true 

A través de la línea de comandos:

java -jar monappli-0.0.1-SNAPSHOT.jar --debug 

Seguidamente, Spring genera un informe completo sobre su configuración automática. 

Por ejemplo, si añadimos Spring Security, se añade una gestión de usuarios y se crea un usuario al inicio. La configuración manual tiene prioridad sobre la configuración automática a medida que avanza el proyecto.

La configuración automática está habilitada si agregamos una anotación @EnableAutoConfiguration o @SpringBootApplication a una de nuestras clases anotadas con @Configuration.

Es posible desactivar parcialmente la autoconfiguración a través del código: Autoconfiguración

import org.springframework.boot.autoconfigure.*; 
                import org.springframework.boot.autoconfigure.jdbc.*; 
                import org.springframework.context.annotation.*; 
                 
                @Configuration 
                @EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}public class MyConfiguration { 
                } 

O mediante el atributo excludeName o la propiedad spring.autoconfigure.exclude.

Como ya se mencionó, la anotación @SpringBootApplication es una composición de las anotaciones @Configuration, @EnableAutoConfiguration y @ComponentScan con sus atributos predeterminados.

Las anotaciones @ComponentScan, @EntityScan y @SpringBootApplication se utilizan para determinar los paquetes candidatos para la detección automática de los Beans.

Utilizamos la configuración a través de la anotación @Configuration evitando la configuración basada en XML tanto como sea posible porque estos archivos hacen que la comprensión general de la aplicación sea menos intuitiva. En general, las aplicaciones que usan Spring Boot son lo suficientemente modernas como para ser configuradas mediante anotaciones sin archivos XML.

Es posible utilizar la anotación @Import para importar Beans que no pertenecen al paquete base de nuestra aplicación:

El siguiente código importa un Bean de la librería Springfox que, por ejemplo, permite hacer Swagger 2:

@Import({ ... 
                springfox.documentation.spring.data.rest.configurationSpringDataRestConfiguration.class, 
                ...}) 

Los starters

Los starters son dependencias que añaden configuración automática a la aplicación basada en Spring Boot. El uso de un starter permite indicar que queremos añadir una funcionalidad a la aplicación y que dejamos que el framework complete nuestra configuración. Para elegir la versión del contenedor, es suficiente con elegir el starter correcto.

A continuación, se muestra una lista de starters. Algunos se describen con más detalle en las siguientes secciones. La lista es bastante larga:

1. Starters comunes

Starter

Utilidad

spring-boot-starter

Es el starter central que incluye soporte para configuración automática, el log y Yaml. Siempre estará presente. Autoconfiguración

spring-boot-starter-aop

Starter para la programación orientada a aspectos (AOP) con Spring AOP y AspectJ.

spring-boot-starter-batch

Starter para usar Spring Batch.

spring-boot-starter-cache

Starter para usar el soporte de caché.

spring-boot-starter-jdbc

Starter para usar JDBC con el pool de conexiones JDBC de Tomcat. Tomcat

spring-boot-starter- jersey

Starter para construir aplicaciones web RESTful usando JAX-RS y Jersey como alternativa a spring-boot-starter-web.

spring-boot-starter- security

Starter para usar Spring Security.

spring-boot-starter-test

Starter para probar aplicaciones Spring Boot con JUnit, Hamcrest y Mockito.

spring-boot-starter- validation

Starter para utilizar Java Bean Validation con Hibernate Validator.

spring-boot-starter-web

Starter para crear aplicaciones web que usan Spring MVC, incluyendo las aplicaciones RESTful. Utiliza Tomcat como contenedor de servlets integrado. Tomcat

spring-boot-starter-web-services

Starter para usar Spring Web Services.

spring-boot-starter-websocket

Starter para construir aplicaciones que utilizan WebSockets.

spring-boot-starter- logging

Starter para logger con Logback. Es un starter predeterminado.

spring-boot-starter- tomcat spring-boot-starter-tomcat

Starter para utilizar Tomcat como contenedor de servlets integrado. Este es el contenedor de servlets predeterminado.

spring-boot-starter-web

Starter para la fabricación de herramientas destinadas a la producción o explotación.

spring-boot-starter-actuator

Starter para usar el Spring Boot’s Actuator, que proporciona una solución llave en mano para monitorizar y administrar la aplicación. Este módulo algunas veces se deshabilita porque, si está mal configurado, puede proporcionar acceso a información importante de la aplicación en el entorno de producción.

spring-boot-starter-remote-shell

Starter para utilizar el CRaSH remote shell para monitorizar y administrar la aplicación. Algunas veces, este módulo está deshabilitado porque, si está mal configurado, puede dar acceso a información importante sobre la aplicación. Ha quedado obsoleto desde la versión 1.5.

spring-boot-starter-jetty spring-boot-starter-jetty

Starter para usar Jetty como contenedor de servlets integrado. Es una alternativa al uso de spring-boot-starter-tomcat.

spring-boot-starter-log4j2

Starter para usar Log4J2 para el logging. Es una alternativa al uso de spring-boot-starter-logging.

spring-boot-starter-undertow spring-boot-starter-undertow

Starter para utilizar Undertow como contenedor de servlets integrado. Es una alternativa al uso de spring-boot-starter-tomcat.

2. Starters orientados a mensajes

Starter

Utilidad

spring-boot-starter- activemq

Starter para usar JMS con Apache ActiveMQ. ActiveMQ

spring-boot-starter-amqp

Starter para usar Spring AMQP y Rabbit MQ.

spring-boot-starter- artemis

Starter para JMS con Apache Artemis.

3. Bases de datos

Starter

Utilidad

spring-boot-starter-data-cassandra

Starter para usar Cassandra. Cassandra

spring-boot-starter-data-couchbase

Starter para usar Couchbase. Couchbase

spring-boot-starter-data-elasticsearch

Starter para usar Elasticsearch.

spring-boot-starter-data-gemfire

Starter para usar GemFire. GemFire

spring-boot-starter-data-jpa

Starter para usar Spring Data JPA con Hibernate.

spring-boot-starter-data-ldap

Starter para usar LDAP de Spring Data.

spring-boot-starter-data-mongodb

Starter para usar MongoDB. MongoDB

spring-boot-starter-data-neo4j

Starter para usar Neo4j. Neo4j

spring-boot-starter-data-redis

Starter para usar Redis. Redis

spring-boot-starter-data-rest

Starter para explorar los datos a través de repositorios DAO Spring Data con REST, utilizando Spring Data REST.

spring-boot-starter-data-solr

Starter para usar el Apache Solr.

4. Servicios web

Starter

Utilidad

spring-boot-starter-hateoas

Starter para crear aplicaciones de web services RESTful basadas en hipermedia con Spring MVC y Spring HATEOAS. Spring HATEOAS

5. Motores de renderizado

Starter

Utilidad

spring-boot-starter-freemarker

Starter para crear aplicaciones web de Spring MVC utilizando plantillas de vista FreeMarker.

spring-boot-starter-groovy-templates

Starter para crear aplicaciones web de Spring MVC utilizando plantillas de vista de Groovy.

spring-boot-starter- mustache

Starter para crear aplicaciones web Spring MVC usando plantillas de vista de Mustache.

spring-boot-starter- thymeleaf

Starter para crear aplicaciones web Spring MVC utilizando plantillas de vista Thymeleaf. Thymeleaf

6. Starters menos comunes

Starter

Utilidad

spring-boot-starter-cloud-connectors

Starter para usar Spring Cloud Connectors. Simplifica la conexión a servicios como Cloud Foundry y Heroku.

spring-boot-starter- integration

Starter para usar Spring Integration.

spring-boot-starter-jooq

Starter para usar jOOQ para acceder a bases de datos SQL como alternativa a spring-boot-starter-data-jpa o spring-boot-starter-jdbc.

spring-boot-starter-jta-atomikos

Starter para usar Atomikos para gestionar transacciones JTA.

spring-boot-starter-jta-bitronix

Starter para usar Bitronix para gestionar transacciones JTA.

spring-boot-starter-jta-narayana

Starter para usar Narayana para gestionar transacciones JTA.

spring-boot-starter-mail

Starter para usar Java Mail y Spring para enviar correos.

spring-boot-starter-mobile

Starter para construir aplicaciones web móviles.

spring-boot-starter-social-facebook

Starter para usar Spring Social Facebook.

spring-boot-starter-social-linkedin

Starter para usar Spring Social LinkedIn.

spring-boot-starter-social-twitter

Starter para usar Spring Social Twitter.

VM6883:64

Spring MVC

Spring MVC con Spring Boot utiliza los siguientes servidores:

Servidor

Versión Servlet

Tomcat 9 Tomcat

4.0

Jetty 9.4 Jetty

3.1

Jetty 10.0

4.0

Undertow 2.0 Undertow

4.0

VM6883:64

Personalización de banners

Cuando la aplicación Spring Boot arranca, se muestra un banner en el log. Este banner es personalizable. Basta con añadir un archivo de texto banner.txt en el classpath o especificar su ubicación a través de banner.location en la configuración. El charset de codificación se puede especificar si no es UTF-8. También es posible utilizar una imagen: banner.gif, banner.jpg o banner.png, que se transforma en ASCII Art durante el arranque.

Podemos utilizar este sitio web para hacer ASCII Art: http://patorjk.com/software/taag

La propiedad spring.main.banner-mode se utiliza para determinar si el banner se muestra en la consola System.out (registro log) o no (en off).

VM6883:64

Eventos de aplicación

Spring utiliza los eventos para notificar ciertas cosas, como la actualización del contexto a través del evento ContextRefreshedEvent:

@Component 
                public class MyListener { 
                 @EventListener 
                 public void handleContextRefresh(ContextRefreshedEvent event) { 
                 ... 
                 } 
                } 

Esto permite notificar a la clase MyListener cuando se ha actualizado el contexto y se puede utilizar para ejecutar código arbitrario cuando el contexto de la aplicación se ha iniciado completamente. Los eventos son importantes para determinar la readiness (la aplicación está preparada para recibir consultas) y la liveness (la aplicación está arrancada).

Una aplicación Spring Boot proporciona los siguientes eventos:

  • El evento ApplicationStartingEvent se envía al inicio de una ejecución, pero antes de cualquier procesamiento, excepto para registrar los listeners y starters.

  • El evento ApplicationEnvironmentPreparedEvent se envía cuando se conoce el entorno que se va a usar en el contexto, pero antes de que se cree el contexto.

  • El evento ApplicationPreparedEvent se envía justo antes de que comience la actualización, pero después de que se carguen las definiciones de los beans.

  • El evento ApplicationReadyEvent se envía después de la actualización y de que se hayan procesado todas las llamadas asociadas para indicar que la aplicación está lista para procesar consultas.

  • El evento ApplicationFailedEvent se envía si hay una excepción durante el arranque.

El comportamiento es diferente con Spring Boot 2.x, como se describe en la guía de migración, al final del capítulo.

VM6883:64

Recuperación de argumentos de la línea de comandos

Hay dos interfaces Spring Boot que permite ejecutar código justo antes de que la aplicación termine de arrancar, a través de dos interfaces: CommandLineRunner y ApplicationRunner. Estas interfaces se llaman justo antes del método run(), después de que finalice SpringApplication. Esto también puede formar parte de la fase de configuración de la aplicación. CommandLineRunner ApplicationRunner

1. CommandLineRunner

Esta interfaz permite obtener los argumentos de la línea de comandos en forma de tabla.

@SpringBootApplication 
                public class SpringBootWebApplication implements CommandLineRunner { 
                 public static void main(String[] args) throws Exception { 
                   SpringApplication.run(SpringBootWebApplication.class, args); 
                 } 
                 @Override 
                 public void run(String... args) throws Exception { 
                   logger.info("Application Started !!"); 
                 } 
                } 

2. ApplicationRunner

Esta interfaz permite acceder a los argumentos a través de la interfaz ApplicationArguments, que expone varias formas de obtener esos argumentos:

  • getOptionNames(): devuelve la lista de nombres de argumentos.

  • getOptionValues(): devuelve la lista de valores de argumento.

  • getSourceArgs(): devuelve la lista de valores de argumento.

@Slf4j 
                public class SpringBootApplicationRunner implements ApplicationRunner { 
                 
                public static void main(String] args) { 
                 SpringApplication.run(SpringBootApplicationRunner.class, args); 
                } 
                 
                @Override 
                public void run(ApplicationArguments args) { 
                 final Set  optionNames = args.getOptionNames(); 
                 final List nonOptionArgs = args.getNonOptionArgs(); 
                 final String] sourceArgs = args.getSourceArgs(); 
                 
                 nonOptionArgs.forEach(nonOption -> log.info("Non Option Args : 
                "+nonOption)); 
                 optionNames.forEach(option -> log.info("Option Names    : "+option)); 
                 Arrays.stream(sourceArgs).forEach(srcArgs ->log.info("Src Args :" 
                   +srcArgs)); 
                 log.info("Option Value of --optionalArg1 : " 
                   +args.getOptionValues("optionalArg1")); 
                 log.info("Option Value of --optionalArg2 :" 
                   +args.getOptionValues("optionalArg2")); 
                 
                } 
                } 

3. La configuración yaml y perfiles

El sistema de configuración properties es importante en Spring Boot.

Es posible utilizar un archivo yaml en lugar de los archivos .properties o XML tradicionales. Este archivo es más compacto y claro porque tiene la forma de un árbol de propiedades.

También puede implementar archivos por entorno con perfiles:

application-{profile}.properties

y

application-{profile}.yml

Normalmente, los archivos están en: /miAplicacion/src/main/resources/application.yml.

Archivo de configuración de la aplicación application.yml

# tag::yaml[] 
                # ------------------------------------------------------------- 
                # - Mi aplicación de ejemplo 
                # ------------------------------------------------------------- 
                application: 
                  name: MIAPL 
                  logs: 
                    dir: e:/logs/miapl 
                    colorised: true 
                instance: DEVMIAPL 
                environment: dev 
                 
                miapl: 
                  public.endpoint: http://localhost:8888/miapl/ 
                # end::yaml[] 

Para acceder a los datos, basta con tener una clase de configuración.

ConfigurationYml.java

@Configuration 
                @EnableConfigurationProperties 
                @ConfigurationProperties 
                 
                @Data 
                public class ConfigurationYml { 
                   private String instance; 
                   entorno privado de cadenas; 
                } 

Entonces es posible acceder a los valores en el código.

VM6883:64

La anotación EnableConfigurationProperties

La anotación @ConfigurationProperties permite definir una clase tipada en la que se pueden inyectar datos.

A partir de la base de la clave, Spring aplica las propiedades a los campos clave. La anotación @EnableConfigurationProperties permite activarla. Esta anotación es de tipo @Import.

Esta anotación permite importar definiciones de Beans en el log inmediatamente después de importar Beans Spring.

Configurar los logs

Spring Boot utiliza Commons Logging para todos los registros de actividad o logs, pero es posible cambiar el comportamiento predeterminado. Se proporcionan configuraciones predeterminadas para Java Util Logging, Log4J2 y Logback. En cada caso, los registros están preconfigurados para utilizar la salida de la consola, junto con la salida de archivos opcional. Log4J2 Logback

Si usa starters, Logback se utilizará de forma predeterminada para los logs. También se incluye el enrutamiento Logback adecuado para garantizar que funcionen correctamente las librerías dependientes que utilizan Java Util Logging, Commons Logging, Log4J o SLF4J. SLF4J Java Util Logging Commons Logging

Tenemos a nuestra disposición los niveles de log clásicos FATAL (excepto para Logback), ERROR, WARN, INFO, DEBUG y TRACE.

1. Los logs de color Log de color

Si no especificamos nada, el log es monocromático, pero es posible añadir color en los logs. El sistema utilizado por defecto es Logback, que soporta colores.

Para ello, debe establecer la variable de configuración: spring.output.ansi.enabled=always.

La primera posibilidad consiste en poner un formato para el log en el archivo de configuración de la aplicación.

En el archivo de configuración:

logging.pattern.console=%d{dd-MM-yyyy HH:mm:ss.SSS} 
                %magenta([%thread]) %highlight(%-5level) %logger.%M - %msg%n 

Aquí creamos registros para verlos en color:

LOGGER.trace("Pruebas de los logs:trace")LOGGER.debug("Pruebas de los logs:debug")LOGGER.info("Pruebas de los logs:info")LOGGER.warn("Pruebas de los logs:warn")LOGGER.error("Pruebas de los logs:error"); 

Log mostrado:

02-03-2018 12:42:27.545 [main] TRACE 
                fr.eni.spring5.logs.Ex1Logs.run - Pruebas de los logs:trace 
                02-03-2018 12:42:27.545 [main] DEBUG 
                fr.eni.spring5.logs.Ex1Logs.run - Pruebas de los logs:debug 
                02-03-2018 12:42:27.545 [main] INFO 
                fr.eni.spring5.logs.Ex1Logs.run - Pruebas de los logs:info 
                02-03-2018 12:42:27.545 [main] WARN 
                fr.eni.spring5.logs.Ex1Logs.run - Pruebas de los logs:warn 
                02-03-2018 12:42:27.545 [main] ERROR 
                fr.eni.spring5.logs.Ex1Logs.run - Pruebas de los logs:error 

Entonces tenemos:

  • para los bloques [main] para el thread en rosa.

  • TRACE y DEBUG en negro.

  • INFO en azul.

  • WARN en naranja.

  • ERROR en rojo.

Otra posibilidad es usar el formato con %clr: %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){yellow}.

También es posible utilizar caracteres de escape para poner color.

El lanzador:

@SpringBootApplication 
                public class Ex1Logs implements CommandLineRunner { 
                  //Logback 
                  private static final Logger LOGGER = 
                LoggerFactory.getLogger(Ex1Logs.class); 
                  //Log4j 
                  //private static Logger LOGGER = LogManager.getLogger(Ex1Logs.class); 
                 
                  public static void main(String[] args) { 
                    SpringApplication.run(Ex1Logs.class, args); 
                  } 
                  @Override 
                  public void run(String... args) throws Exception { 
                    LOGGER.trace("Pruebas de los logs:trace"); 
                    LOGGER.debug("Pruebas de los logs:debug"); 
                    LOGGER.info("Pruebas de los logs:info"); 
                    LOGGER.warn("Pruebas de los logs:warn"); 
                    LOGGER.error("Pruebas de los logs:error"); 
                 
                   LOGGER.info(Coloreada.GREEN_BOLD + "Texto en verde negrita " + 
                Coloreada.RESET); 
                    LOGGER.info(Coloreada.RED + "Texto en rojo " + Coloreada.RESET); 
                    LOGGER.info(Coloreada.GREEN_BACKGROUND + Coloreada.RED + 
                "Texto en rojo sobre fondo verde" + Coloreada.RESET); 
                  } 
                } 

En el ejemplo, hay comentados varios proveedores de log para experimentar.

Clase Coloreada:

Podemos usar esta clase para tener constantes que se definen en forma literal.

//https://stackoverflow.com/questions/5762491/how-to-print-color-in- 
                console-using-system-out-println 
                public class Coloreada { 
                  // Reset 
                  public static final String RESET = "\033[0m";  // Text Reset 
                 
                  // Regular Colors 
                  public static final String BLACK = "\033[0;30m";   // BLACK 
                  public static final String RED = "\033[0;31m";     // RED 
                  public static final String GREEN = "\033[0;32m";   // GREEN 
                  public static final String YELLOW = "\033[0;33m";  // YELLOW 
                  public static final String BLUE = "\033[0;34m";    // BLUE 
                  public static final String PURPLE = "\033[0;35m";  // PURPLE 
                  public static final String CYAN = "\033[0;36m";    // CYAN 
                  public static final String WHITE = "\033[0;37m";   // WHITE 
                 
                  // Bold 
                  public static final String BLACK_BOLD = "\033[1;30m";  // BLACK 
                  public static final String RED_BOLD = "\033[1;31m";    // RED 
                  public static final String GREEN_BOLD = "\033[1;32m";  // GREEN 
                  public static final String YELLOW_BOLD = "\033[1;33m"; // YELLOW 
                  public static final String BLUE_BOLD = "\033[1;34m";   // BLUE 
                  public static final String PURPLE_BOLD = "\033[1;35m"; // PURPLE 
                  public static final String CYAN_BOLD = "\033[1;36m";   // CYAN 
                  public static final String WHITE_BOLD = "\033[1;37m";  // WHITE 
                 
                  // Underline 
                  public static final String BLACK_UNDERLINED = "\033[4;30m";  // BLACK 
                  public static final String RED_UNDERLINED = "\033[4;31m";    // RED 
                  public static final String GREEN_UNDERLINED = "\033[4;32m";  // GREEN 
                  public static final String YELLOW_UNDERLINED = "\033[4;33m"; // YELLOW 
                  public static final String BLUE_UNDERLINED = "\033[4;34m";   // BLUE 
                  public static final String PURPLE_UNDERLINED = "\033[4;35m"; // PURPLE 
                  public static final String CYAN_UNDERLINED = "\033[4;36m";   // CYAN 
                  public static final String WHITE_UNDERLINED = "\033[4;37m";  // WHITE 
                 
                  // Background 
                  public static final String BLACK_BACKGROUND = "\033[40m";  // BLACK 
                  public static final String RED_BACKGROUND = "\033[41m";    // RED 
                  public static final String GREEN_BACKGROUND = "\033[42m";  // GREEN 
                  public static final String YELLOW_BACKGROUND = "\033[43m"; // YELLOW 
                  public static final String BLUE_BACKGROUND = "\033[44m";   // BLUE 
                  public static final String PURPLE_BACKGROUND = "\033[45m"; // PURPLE 
                  public static final String CYAN_BACKGROUND = "\033[46m";   // CYAN 
                  public static final String WHITE_BACKGROUND = "\033[47m";  // WHITE 
                 
                  // High Intensity 
                  public static final String BLACK_BRIGHT = "\033[0;90m";  // BLACK 
                  public static final String RED_BRIGHT = "\033[0;91m";    // RED 
                  public static final String GREEN_BRIGHT = "\033[0;92m";  // GREEN 
                  public static final String YELLOW_BRIGHT = "\033[0;93m"; // YELLOW 
                  public static final String BLUE_BRIGHT = "\033[0;94m";   // BLUE 
                  public static final String PURPLE_BRIGHT = "\033[0;95m"; // PURPLE 
                  public static final String CYAN_BRIGHT = "\033[0;96m";   // CYAN 
                  public static final String WHITE_BRIGHT = "\033[0;97m";  // WHITE 
                 
                  // Bold High Intensity 
                  public static final String BLACK_BOLD_BRIGHT = "\033[1;90m"; // BLACK 
                  public static final String RED_BOLD_BRIGHT = "\033[1;91m";   // RED 
                  public static final String GREEN_BOLD_BRIGHT = "\033[1;92m"; // GREEN 
                  public static final String YELLOW_BOLD_BRIGHT = "\033[1;93m";// YELLOW 
                  public static final String BLUE_BOLD_BRIGHT = "\033[1;94m";  // BLUE 
                  public static final String PURPLE_BOLD_BRIGHT = "\033[1;95m";// PURPLE 
                  public static final String CYAN_BOLD_BRIGHT = "\033[1;96m";  // CYAN 
                  public static final String WHITE_BOLD_BRIGHT = "\033[1;97m"; // WHITE 
                 
                  // High Intensity backgrounds 
                  public static final String BLACK_BACKGROUND_BRIGHT = 
                "\033[0;100m";// BLACK 
                  public static final String RED_BACKGROUND_BRIGHT = "\033[0;101m";// RED 
                  public static final String GREEN_BACKGROUND_BRIGHT = 
                "\033[0;102m";// GREEN 
                  public static final String YELLOW_BACKGROUND_BRIGHT = 
                "\033[0;103m";// YELLOW 
                  public static final String BLUE_BACKGROUND_BRIGHT = 
                "\033[0;104m";// BLUE 
                  public static final String PURPLE_BACKGROUND_BRIGHT = 
                "\033[0;105m"; // PURPLE 
                  public static final String CYAN_BACKGROUND_BRIGHT = 
                "\033[0;106m";  // CYAN 
                  public static final String WHITE_BACKGROUND_BRIGHT = 
                "\033[0;107m";   // WHITE 
                } 

Podemos usar este método para localizar una secuencia particular de logs, que seguidamente tendrá un color claramente visible, lo que facilitará sobremanera la búsqueda en los registros de actividad.

2. Elección del tipo de registro

El sistema utiliza nombres de archivo para elegir el sistema de log. Spring Boot unifica los logs.

Lista de frameworks habituales de log:

Sistema

Archivos

Logback Logback

logback-resort.xml

Log4J2 Log4J2

log4j2-Spring.xml

JDK (JUL) JUL

logging.properties

El Log4J se realiza a través de SLF4J.

Para Logback, la configuración se debe realizar en un archivo logback-spring.xml. Spring inicializa incorrectamente el sistema de registro si se utiliza el archivo logback.xml. Sin embargo, podemos tener ambos para las partes no Spring, que se encargan del registro de actividad.

Configuración automática para Spring MVC Configuración automática

Spring ofrece configuración automática de Spring MVC a través del starter spring-boot-starter-web.

Esto provoca que se tenga en cuenta:

  • La inclusión del bean ContentNegotiatingViewResolver y del bean BeanNameViewResolver.

  • Soporte para recursos estáticos, index.html, Favicon personalizado y WebJars

  • Registro automático de beans Converter, GenericConverter, Formatter y MessageCodesResolver.

  • Soporte de los HttpMessageConverters y uso automático de un bean ConfigurableWebBindingInitializer.

Es posible mantener solo las funcionalidades de Spring MVC añadiendo los beans de configuración interceptores, formateadores, controladores de vista mediante la creación de nuestra propia clase @Configuration de tipo WebMvcConfigurerAdapter, pero luego no debemos usar la anotación @EnableWebMvc. Se puede proporcionar una instancia personalizada de RequestMappingHandlerMappingRequestMappingHandlerAdapter o ExceptionHandlerExceptionResolver declarando una instancia de WebMvcRegistrationsAdapter. También podemos tomar el control completo de Spring MVC añadiendo nuestra propia clase de configuración (anotada por @Configuration) anotada con @EnableWebMvc.

Gestión de sesiones Sesión

Intentamos ser stateless porque la gestión de sesiones dificulta la agrupación en clústeres de servidores debido al uso compartido de sesiones. Por lo tanto, «emulamos» la sesión a través de una caché compartida.

En algunos casos no tenemos otra opción; estamos obligados a gestionar sesiones. Las sesiones se pueden administrar en el proyecto Spring Session, que tiene dos versiones en paralelo. El objetivo del proyecto es ofrecer una alternativa a las sesiones de contenedores Tomcat, Jetty, etc. Tomcat Jetty

El módulo Spring Session gestiona las sesiones.

Existen diferencias entre las dos implementaciones.

Versión

Información

HttpSession

WebSocket

WebSession

1.5

Spring 4.x

no

2.6

Spring 5.x

En esta tabla, Spring 4 y 5 se corresponden con las versiones 1.5 y 2.6 de Spring Boot.

HttpSession (Spring): sustituye a HttpSession en un contenedor (Tomcat) de forma neutral añadiendo las siguientes funcionalidades:

  • Sesiones clusterizadas.

  • Sesión de navegadores múltiples: varios usuarios conectados en una misma instancia del navegador.

  • API RESTful: identificadores de sesión en los headers.

WebSocket: conserva la sesión HttpSession cuando recibimos mensajes WebSocket.

WebSession: permite sustituir los WebSession de una aplicación en contenedores Spring WebFlux.

Para la sesión HttpSession, podemos colocar las sesiones en estos contenedores:

Contenedor

Utilidad

Redis

La sesión está en un registro Redis que es muy rápido y sencillo.

JDBC

La sesión está en una base de datos.

Hazelcast

La sesión está en Hazelcast.

Si usamos el ejemplo de ex2security y lo modificamos para usar Redis para poner las sesiones, es suficiente con tener spring-session-data-redis en el pom.xml y añadir spring.session.store-type=redis en la configuración de la aplicación, y las sesiones se colocan en Redis.

Es posible usar lettuce en lugar de Jedis. Para esto, hay que añadir la dependencia a lettuce: lettuce Jedis

<dependency> 
                  <groupId>biz.paluch.redis</groupId> 
                  <artifactId>lettuce</artifactId> 
                  <version>4.5.0.Final</version> 
                </dependency> 

y activar la factory Lettuce:

@EnableRedisHttpSession 
                public class ConfigurationRedisHttpSession { 
                  @Bean 
                  public LettuceConnectionFactory connectionFactory() { 
                    return new LettuceConnectionFactory(); 
                  } 
                } 

Guía de migración de la versión 1.5 a la versión 2.x

En términos de migración de Spring Boot 1.5 a la versión 2.x, la documentación de Spring enumera una serie de puntos.

1. Archivos de configuración

Algunas propiedades se han cambiado entre la versión 1.5 y la versión 2.x. Se facilita un starter específico para simplificar la migración. Garantiza una compatibilidad relativa con versiones anteriores durante la migración.

<dependency> 
                 <groupId>org.springframework.boot</groupId> 
                 <artifactId>spring-boot-properties-migrator</artifactId> 
                </dependency> 

2. Diferentes comportamientos

Las aplicaciones Spring Boot ahora se pueden ejecutar en nuevos modos. Por esta razón, la propiedad spring.main.web-environment ahora está obsoleta y se reemplaza por spring.main.web-application-type, que proporciona más control.

3. Arranque

Es posible retrasar el arranque del servidor web hasta que se inicie la aplicación cambiando la propiedad spring.main.web-application-type=none o utilizar setWebApplicationType en SpringApplication para hacerlo mediante programación.

4. Utilizar ApplicationRunner o CommandLineRunner

En la versión 1.x, los beans ApplicationRunner y CommandLineRunner se invocan al inicio del proceso y esto ha causado problemas porque los runners no habían terminado su lanzamiento. En la versión 2.x, se invocan una vez que ha terminado el proceso de arranque y el conector HTTP comienza solamente a aceptar consultas. Si estaba utilizando esta notificación para inicializar algo antes de que el conector HTTP comience a aceptar solicitudes, debe actualizar el código para usar un método en su lugar con la anotación @PostConstruct. Si desea llamar a algo lo más tarde posible justo antes de que se inicie el conector, puede implementar un SmartInitializingBean. ApplicationRunner CommandLineRunner SmartInitializingBean

5. Configuración externalizada

Para las propiedades de configuración seguras, las reglas relativas a Relaxed binding se han endurecido. Supongamos una propiedad existente eni.mi-proyecto.mi-aplicacion:

  • Todos los prefijos deben estar en formato kebab-case (minúsculas, separados por un guion de unión), inicio.miPrograma o inicio.mi_programa no son válidos. kebab-case

  • Los nombres de propiedad pueden usar los formatos kebab-case, camel-case o snake-case: mi-nombre, miNombre, mi_nombre son válidos. camel-case snake-case

  • Las propiedades de entorno (de las variables de entorno del sistema operativo) deben usar el formato habitual de subrayado con mayúsculas, donde el carácter de subrayado solo se debe utilizar para separar partes de la clave: por ejemplo, MI_APLI_MI_PROYECTO_MI_MICROSERVICIO.

Estos Relaxed binding tienen varias ventajas: no hay necesidad de preocuparse por la estructura de la clave en @ConditionalOnProperty. Siempre que la clave esté definida en el formato canónico, las variantes relajadas soportadas funcionarán de forma transparente. Si utiliza el atributo prefix, ahora simplemente puede poner la clave completa utilizando los atributos name o value.

RelaxedPropertyResolver ya no está disponible porque el entorno se encarga de él automáticamente: env.getProperty("en.eni.spring5") encontrará una propiedad en.eni.spring5.

La clase RelaxedDataBinder se sustituye por una nueva clase denominada Binder: RelaxedDataBinder

new Binder(ConfigurationPropertySources.from(propertySource)) + 
                . bind("en.eni.Spring5", bindable.of(target))) 

6. Desarrollo de aplicaciones web

Web Starter como dependencia transitiva (lazy)

Anteriormente, había disponibles varios starters Spring Boot de manera temporal y transitoria en función de Spring MVC con spring-boot-starter-web. Con el nuevo soporte para Spring WebFlux, spring-boot-starter-moustache, spring-boot-starter-freemarker y spring-boot-starter-thymeleaf ya no dependen de spring-boot-starter-web. Es responsabilidad del desarrollador elegir y añadir spring-boot-starter-web o spring-boot-starter-webflux. WebFlux spring-boot-starter-webflux

7. Seguridad

Spring Boot 2 simplifica enormemente la configuración de seguridad predeterminada y facilita la adición de la gestión de seguridad personalizada. En lugar de disponer de varias configuraciones automáticas relacionadas con la seguridad, Spring Boot ahora tiene un comportamiento único que se detecta automáticamente, tan pronto como añade su propio WebSecurityConfigurerAdapter. WebSecurityConfigurerAdapter

También se detecta mediante una de las siguientes propiedades:

  • security.basic.authorize-mode

  • security.basic.enabled

  • security.basic.path

  • security.basic.realm

  • security.enable-csrf

  • security.headers.cache

  • security.headers.content-security-policy

  • security.headers.content-security-policy-mode

  • security.headers.content-type

  • security.headers.frame

  • security.headers.hsts

  • security.headers.xss

  • security.ignored

  • security.require-ssl

  • security.sessions

En este caso de la migración, la seguridad predeterminada a través de la configuración automática ya no expone las opciones y utiliza, en la medida de lo posible, los valores predeterminados de Spring Security. El uso de la negociación de contenido de Spring Security para la autorización (formulario de conexión) cambia para el usuario predeterminado. Spring Boot ya no configura un usuario porque las propiedades security.user.* ya no existen. En su lugar, necesitamos exponer un bean UserDetailsService.

Descripción avanzada de Spring Boot

En este apartado se describe cómo iniciar un programa Spring Boot. Sirve para desmitificar una vez más la aparente «magia» del sistema que, en realidad, es el resultado de un trabajo interesante y concienzudo. Spring utiliza los mecanismos de Spring Boot que pone a nuestra disposición.

Las fuentes de Spring Boot se pueden ver en https://github.com/spring-projects/spring-boot. Podemos estudiar el comportamiento de Spring Boot durante el arranque a partir de estas fuentes. Una aplicación Spring Boot se caracteriza por dos elementos: la anotación @SpringBootConfiguration y el método estático RUN dentro de un programa Java estándar:

@SpringBootApplication 
                public class Application public static void main(String[] args) {  
                 SpringApplication.run(Application.class, args); 
                 } 
                } 

La anotación @SpringBootApplication es una composición de las anotaciones @SpringBootConfiguration, @EnableAutoConfiguration y @ComponentScan con sus atributos predeterminados.

Como hemos visto, la anotación @SpringBootConfiguration es un alias para la anotación @Configuration y la anotación @EnableAutoConfiguration indica que tenemos la configuración incorporada o incrustada en el código. La anotación @ComponentScan indica que el paquete que contiene la clase principal sirve como paquete raíz para el escaneo de los componentes de Spring. La anotación @EnableAutoConfiguration es una anotación de Spring Boot que indica que usaremos la mecánica de configuración automática. A través del análisis de las fuentes del método estático RUN, la secuencia de arranque indica que Spring Boot se inicia siguiendo estas etapas:

  • Carga del BootstrapContext

  • Listeners

  • Entorno

  • Creación del ApplicationContext

  • Prepare, Refresh & PostRefresh del contexto

  • Ejecución de los runners

Cargar el BootstrapContext

Spring Boot comienza inicializando el BootstrapContext, que es un minicontexto Spring temporal depurado para la fase de inicialización. El BootstrapContext se introdujo con Spring Boot en su versión 2.4 para facilitar la implementación de los módulos Spring Cloud. Permite que el framework prepare el contexto de la aplicación. Este contexto se basa en dos interfaces: BootstrapRegistry, que administra las escrituras en el contexto, y BootstrapContext, que administra la parte de la lectura.

BootstrapRegistry: registra la clase en el contexto usando el método register

BootstrapContext: carga la clase en el contexto usando el método get(Class <T> type).

Este contexto es más sencillo porque permite asociar una clase solo con un tipo.

Ejemplo de utilización:

//Escritura en el contexto: 
bootstrapContext.register(MiClase.classInstanceSupplier.from(MiClase::new).withScope(Scope.SINGLETON)) ; 

Lectura en el contexto:

Una vez que ha arrancado Spring, este llama al addCloseListener del bootstrap context para guardar los beans en el contexto real Spring de la aplicación.

Una vez cargado el contexto, Spring prepara al listener de inicio para controlar la parte del evento.

Configuración de los listeners

Spring Boot usa la interfaz SpringApplicationRunListener que tiene EventPublishingRunListener como implementación.

Este es el listener que va a enviar los eventos durante la fase de inicialización:

Evento

Significado

starting

ApplicationStartingEvent

environmentPrepared

ApplicationEnvironmentPreparedEvent

contextPrepared

ApplicationContextInitilized+Event

ContextLoaded

ApplicationPreparedEvent

started

ApplicationStartedEvent

running

ApplicationReadyEvent

failed

ApplicationFailedEvent

Ya hemos presentado algunos de estos eventos.

1. El starting: ApplicationStartingEvent

Es posible registrar listeners, como vimos en el capítulo sobre los listeners:

class MiListener implements ApplicationListener<ApplicationStartingEvent> { 
                 void onApplicationEvent(ApplicationStartingEvent event {...} 
                } 

Dado que en este preciso momento del arranque Spring aún no tiene el contexto normal de la aplicación, se basará en el BootstrapContext, que podemos alimentar de tres maneras:

  • Utilizando la carga de factories a través de la mecánica del spring.factories, con la adición de una fila a org.springframework.context.ApplicationListener.

  • Utilizando los métodos SpringApplication.addListeners(...) y SpringApplicationBuilder.listeners(...).

  • Añadiendo el listener al classpath durante el inicio: -Dcontext.listener.classes=...

La propagación de eventos se realiza en la implementación predeterminada, creando y usando un initialMulticaster en el que Spring registra todos los listeners. A continuación, hay publicación y envío (dispatch) de eventos para todos los eventos, hasta llegar al evento started, después del cual hay un giro hacia el contexto real de la aplicación, que es el que hace el dispatch de los eventos en sí.

Justo después del inicio, Spring gestiona el entorno.

2. El entorno: ApplicationEnvironmentPreparedEvent

El entorno Spring Boot permite configurar la aplicación en función de su entorno. Hay varias formas posibles de configurar este entorno y Spring toma el primer valor consultando en orden:

Tipo

Propiedades

A

Argumentos de la línea de comandos

B

La propiedad SPRING_APPLICATION_JSON

C

Argumentos de inicialización del ServletConfig

D

Argumentos de inicialización del ServletContext

E

Atributos JNDI de java:comp/env

F

Propiedades del System Java: System.getProperties

G

Variables de entorno del sistema operativo host

H

RandomValuePropertySource (random.*)

I

Los perfiles application-{profile}.properties o yaml fuera del jar

J

Las application.properties fuera del jar

K

Las application-{profile}.properties o yaml

L

application.properties

M

La anotación @PropertySource en la clase anotada @Configuration.

N

Las propiedades por defecto: SpringApplication.setDefaultProperties(...)

Durante el arranque, Spring Boot consulta ubicaciones y crea un ConfigurableEnvironment llamando al método getOrCreateEnvironment().

En función del tipo de aplicación, Spring crea tres tipos de entornos:

Entorno

Entorno Spring

Reactive

ApplicationReactiveWebEnvironment

Servlets

ApplicationServletEnvironment

Default

ApplicationEnvironment

A continuación, ilustramos el inicio con el ApplicationServletEnvironment, que es el más utilizado en la actualidad.

Spring comienza con las categorías F y G. A continuación, coloca en la cadena las fuentes de configuración C, D y E que se corresponden con la configuración del servidor de servlets embebido de tipo Tomcat o Jetty. En este preciso momento, el servidor no está configurado, por lo que Spring pasa por un Stub StubPropertySource que se sustituirá posteriormente y que cargará los parámetros reales cuando se inicie el servidor.

A continuación, Spring utiliza los argumentos de la línea de comandos A y, seguidamente, las propiedades predeterminadas N.

3. El EnvironmentPostProcessorApplicationListener

Este paso del boot se basa, para los otros orígenes de configuración, en:

  • ConfigDataEnvironmentPostProcessor

  • RandomVAluePropertySourceEnvironmentPostProcessor (H)

  • SystemEnvironmentPropertySourceEnvironmentPostProcessor

Dado que el sistema de log aún no está configurado, Spring utiliza una mecánica de log diferida.

Configuración de logs

Tenemos tres eventos:

Evento

Acciones

ApplicationStartingEvent

Inicio del sistema de logs (LoggingSystem)

ApplicationEnvironmentPreparedEvent

Elección de los niveles de log

ApplicationPreparedEvent

Registro del logger en el contexto

ApplicationStartingEvent configura el log por orden de preferencia para logback, Log4j y LogManager del JDK.

ApplicationEnvironmentPreparedEvent recupera niveles de log por paquete, como por ejemplo logging.level.root=info.

En ese momento creamos el verdadero contexto Spring.

Creación de ApplicationContext

Spring escanea todos los paquetes (con ASM) y utiliza BeanProcessors.

BeanFactoryPostProcessor utiliza ConfigurationClassPostProcessor para identificar y administrar los Beans Spring @Configuration, @Component, @Repository, @Service y @Controller.

BeanPostProcessor utiliza AutowiredAnnotationBeanPostProcessor para Autowire: @Autowired, @Value, constructores, CommonAnnotationBeanPostProcessor para JSR-250: el @Resource, @PostConstruct, PersistenceAnnotationBeanPostProcessor para JPA y el EventListenerMethodProcessor y DefaultEventListenerFactory para los @EventListener.

Preparación y refresco del contexto

Para la fase de preparación, el contexto BootstrapContext se copia de nuevo en el contexto normal y se emite el evento contextPrepared. A continuación, se cierra BootstrapContext, se añade al contexto la definición del Bean principal (que contiene el main) y se emite el evento contextLoaded.

Durante la fase de refresh, se inicializa el contexto Spring y se lanza el contenedor web (Jetty, Tomcat, etc.) sin permitir conexiones.

Los stubs de propiedades se reemplazan y se crean los beans (non lazy). A continuación, Sping indica al servidor embebido que puede aceptar conexiones y se inicia el evento ContextRefreshEvent.

En este preciso momento, el contexto Spring está cargado, pero sigue siendo la parte afectada por la autoconfiguración (anotación @EnableAutoConfiguration).

EnableAutoConfiguration

La anotación EnableAutoConfiguration es de tipo Import.

@Target(ElementType.TYPE) 
                @Retention(RetentionPolicy.RUNTIME) 
                @Documented 
                @Inherited 
                @AutoConfigurationPackage  
                @Import(AutoConfigurationImportSelector.class) 
                public @interface EnableAutoConfiguration { 
                 
                        /** 
                        * Environment property that can be used to override 
                when auto-configuration is 
                        * enabled. 
                        */ 
                        String ENABLED_OVERRIDE_PROPERTY = 
                "spring.boot.enableautoconfiguration"; 
                 
                        /** 
                        * Exclude specific auto-configuration classes such that  
                they will never be applied. 
                        * @return the classes to exclude 
                        */ 
                        Class<?>[] exclude() default {}; 
                 
                        /** 
                        * Exclude specific auto-configuration class names such that 
                they will never be 
                        * applied. 
                        * @return the class names to exclude 
                        * @since 1.3.0 
                        */ 
                        String[] excludeName() default {}; 
                 
                } 

Esto significa que la clase anotada permite importar configuraciones. Spring utiliza un ImportSelector para tener la lista de configuraciones automáticas. Estas configuraciones automáticas se cargan después de cargar la configuración a través de un DeferredImportSelector.

A continuación, Spring Boot determina la lista de configuraciones candidatas para la configuración automática consultando el SpringFactoriesLoader para la clase EnableAutoConfiguration.class.

Como se mencionó anteriormente, el spring.factory del spring-boot-autoconfigure contiene muchos candidatos (131 en el archivo AutoConfiguration.imports).

Las configuraciones se cargan y, gracias a la anotación @Conditionnal y a las @ConfigurationProperties, todo es autoconfigurable. Después de la configuración automática, Spring emite el evento started.

Lanzamiento de runners

Justo después de que se emita el evento started, Spring lanza los runners.

Los runners se utilizan principalmente para las aplicaciones en línea de comandos. Los runners son de dos tipos, que ya hemos presentado: el ApplicationRunner y el CommandLineRunner.

Después de ejecutar el código del run, Spring emite el evento Running.

Esto concluye el ciclo de lanzamiento de la aplicación Spring Boot.

Puntos clave

  • Spring Boot se utiliza para aplicaciones que están en producción.

  • Spring Boot reduce significativamente el código que hay que producir.

  • Spring Boot permite hacer componentes autoconfigurables.

Introducción

Históricamente, las grandes empresas de la Web han necesitado bases de datos cada vez más extensas debido a un gran número de usuarios o volúmenes muy grandes de datos. Hace algún tiempo, el escalado generalmente se realizaba aumentando la capacidad de la máquina que alojaba la base de datos. De hecho, en una base de datos relacional SQL, las relaciones se establecen a través de claves primarias a las que se hace referencia mediante claves secundarias con la ayuda de índices. Las consultas sobre los datos se realizan mediante uniones (joins) y pasado un tiempo, si hay demasiadas uniones, es fácil que el esquema se vuelva demasiado complejo rápidamente. SQL Unión NoSQL

La complejidad relativa a las uniones es un argumento que encontramos muy habitualmente. Debe saber que hay aplicaciones compuestas por varios miles de tablas que utilizan combinaciones que funcionan muy bien en Oracle. Oracle puede administrar miles de millones de metadatos (Dictionary-managed database objects: https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/logical-database-limits.html#GUID-685230CF-63F5-4C5A-B8B0-037C566BDA76). Hay un volumen limitado de datos y un crecimiento exponencial de las dificultades para explotar la base de datos en función de su tamaño.

Los costes se estaban volviendo demasiado altos y, en lugar de usar una sola máquina, se intentó distribuir los tratamientos en un clúster de máquinas pequeñas. Como las bases de datos SQL no encajaban bien en esta arquitectura, se abandonaron algunas funcionalidades en favor de otras.

Esta afirmación se debe matizar porque los grandes proveedores de soluciones integradas en la nube, como Google, ahora proporcionan bases de datos SQL de alto rendimiento a gran escala. Estas bases de datos son gestionadas y, por lo tanto, tienen un coste por uso. Es muy difícil hacer una implementación propia de base de datos SQL autogestionada, mientras que es muy sencillo hacerlo con bases de datos NoSQL.

El escalado requirió algunos sacrificios en términos de funcionalidad. Las bases de datos NoSQL no son relacionales, no tienen esquema y no se describen con SQL. En particular, las utilizamos para almacenar datos en formatos fluctuantes, a menudo basados en texto o JSON binario. En principio, con frecuencia son open source y modernas. En los primeros días de NoSQL, solo los grandes nombres en la Web tenían la capacidad de iniciar la transición de SQL a NoSQL y lo hicieron con su propia implementación de base de datos.

Google formó parte de la iniciativa de Bigtable (orientado a columnas), y Amazon, de Dynamo (enfoque clave-valor). La arquitectura de la mayoría de las bases de datos NoSQL actuales se inspiran en las arquitecturas de estas dos bases de datos históricas. Dynamo

Se desarrollaron diferentes modelos, cada uno con sus ventajas e inconvenientes. Veremos que existe principalmente el modelo basado en la pareja clave-valor, el modelo de documentos, el modelo orientado a columnas y el modelo grafo.

Modelos de datos

Hay cuatro modelos principales:

images/cap15_pag3.png

1. Modelo clave-valor

Piense en estas bases de datos como un gran mapa hash (hashmap) almacenado en disco, con la capacidad de establecer metadatos para crear índices. Hay tres grandes bases de datos de este tipo: Project Voldemort, que es un sistema de archivos distribuido propuesto por LinkedIn y que consiste en una implementación gratuita del sistema Dynamo de Amazon; Riak, que está inspirado en Dynamo, y Redis REmote DIctionary Server (servidor de diccionario remoto). Project Voldemort Riak Redis

2. Modelo Documentos

En este modelo básico, cualquier documento tiene una estructura compleja persistente (guardada) a la vez. La base de datos está orientada a datos agregados. Necesitamos agrupar las cosas que naturalmente forman parte de un agregado y hacer que persistan en la base de datos. Hay una tendencia a hacer un esquema implícito. Los datos suelen representar entidades concretas. Cabe señalar que, en un sistema clusterizado, si un agregado apunta a otro, el conjunto puede estar en varios nodos.

En una base de datos agregada, la organización del agregado está centrada y optimizada desde el punto de vista de la observación del agregado. Si cambiamos el punto de vista, debemos reorganizar o hacer una composición compleja no optimizada. El identificador de un registro es el equivalente de un sistema clave-valor.

Los agregados son uno de los conceptos centrales del enfoque moderno de DDD (Domain Driven Design). Domain Driven Design DDD

Las bases de datos más conocidas de este tipo son MongoDB, CouchDB y RavenDB.  RavenDB CouchDB MongoDB

3. Modelo Orientado a columnas

Los datos se almacenan como columnas.

En este tipo de bases de datos se facilitan los aspectos relacionados con la búsqueda. Las bases más conocidas de este tipo son Cassandra y Hbase. Cassandra Hbase

Base relacional:

 

entrante

plato

postre

bebida

1

ensalada

pollo

pastel

 

2

 

patatas fritas

 

 

3

 

 

nata

café

Columna base:

1

Entrante/ensalada

Plato/pollo

postre/tarta

2

Plato/patatas fritas

 

 

3

Postre/nata

Bebida/café

 

La base de datos se optimiza cuando hay más columnas (varios millones) que filas y no todas las columnas están llenas.

4. Bases de datos orientadas a grafos

La base de datos Neo4j está orientada a grafos. Como su nombre indica, almacena sus datos en forma de grafos. Neo4j

Aspectos principales de la base de datos

Consistencia de los datos

La consistencia está pensada para gestionar los cambios concurrentes. En una base de datos, la coherencia suele estar garantizada por la noción de transacción. Si dividimos un conjunto de información en varias pseudotablas o documentos, queremos que, si uno de los cambios falla, todos los cambios se cancelen. Queremos una actualización atómica. La transacción «unitaria» se ha convertido, después de muchos años, en aceptable y aceptada.

Históricamente, en aplicaciones que involucran múltiples sistemas distribuidos o separados, la noción de transacción era central y teníamos transacciones multifase. Como tal, Spring contiene una gestión de transacciones muy completa que permite gestionar una transacción en una serie de llamadas. Hoy en día, con las aplicaciones sin Stateless, las transacciones son mucho más simples: una transacción, a menudo implícita, por llamada. La transacción «unitaria» se ha convertido, después de muchos años, en aceptable y aceptada.

Para las bases de datos orientadas a documentos, la transacción se debe mantener en el nivel del agregado. La actualización de un agregado puede ser atómica, pero tan pronto como se actualizan varios agregados, se puede producir un conflicto.

La localización de datos se puede centrar en una máquina o distribuirse en varias. En el caso de uso de varias máquinas, puede haber un problema de sharding, es decir, dificultades para distribuir los datos de forma inteligente entre máquinas.

Entonces, la dificultad de la consistencia está relacionada con la ubicación de los datos y su distribución en varias máquinas. De hecho, el tipo de consistencia puede ser lógica o basarse en replicaciones para obtener más rendimiento y resiliencia. Si el sistema está desconectado, entonces el dilema entre consistencia y disponibilidad se debe resolver con opciones de negocio.

El teorema CAP Teorema CAP

El teorema CAP o CDP, también conocido como teorema de Brewer, dice que es imposible, en un sistema informático de cálculo distribuido, garantizar de manera síncrona las siguientes tres restricciones:

  • Consistencia (Consistency en inglés): todos los nodos del sistema ven exactamente los mismos datos al mismo tiempo.

  • Disponibilidad (Availability en inglés): garantiza que se respondan todas las solicitudes.

  • Tolerancia de partición (Partition Tolerance en inglés): ningún fallo menos importante que una interrupción total de la red debería impedir que el sistema responda correctamente.

El teorema CAP indica que favorecer la consistencia o disponibilidad en un sistema con una partición de red debido a un sistema distribuido implica una multitud de sutilezas en la elección para las cuales se debe balancear entre consistencia y tiempo de respuesta. Hay que elegir entre seguridad y capacidad de respuesta. En general, preferimos responder rápidamente, incluso si esto implica tener un sistema que permita, a posteriori, ponerse al día con las inconsistencias transitorias.

Principales bases de datos y CAP:

images/cap15_pag9.png

Las tecnologías NoSQL algunas veces requieren más código que su equivalente SQL, inducen cambios en la visibilidad de ciertos datos modificados y se ponen al día. A menudo, esto es posible en sitios de Internet donde el usuario detecta errores en su cesta, por ejemplo. Un sistema de ventas a gran escala puede favorecer la posibilidad de compra, especialmente porque el comprador verificará la consistencia él mismo y se pondrá al día con las alteraciones de su cesta (línea duplicada o perdida). Por otro lado, en otros casos esto será imposible, como por ejemplo en un entorno bancario, donde se dará prioridad a la coherencia de los servicios en línea. Los bancos utilizan NoSQL para procesar grandes volúmenes, para hacer estadísticas o detectar comportamientos en un contexto para el que la pérdida de algunos datos será «aceptable».

Por qué y cuándo usar una base de datos NoSQL

La base de datos NoSQL encuentra su lugar allá donde hay naturalmente agregados y donde hay grandes volúmenes de datos. Para las grandes bases de datos muy voluminosas, se usa NoSQL porque no hay una base de datos SQL lo suficientemente grande.

Las siguientes tablas se pueden utilizar para tener una idea de las bases de datos y sus usos:

Consultas rápidas y fáciles:

Volumen

CAP

Base

Aplicaciones

Débil

En RAM

Redis

Caché

 

En RAM

Memcache

 

Ilimitado

AP

Cassandra

Cesta compra

 

Riak

Tienda

Voldemort

 

Aerospike

 

 

CP

Hbase

Histórico

 

MongoDB

de ventas

CouchBase

 

DynamoDB

 

Consultas complejas:

Volumen

Tipo

Base

Aplicaciones

Disco duro

ACID

RDBMS

Tratamiento

 

Neo4j

transacción

RavenDB

en línea

MarkLogic

 

 

Disponibilidad

CouchDB

Sitio web

 

MongoDB

 

 

SimpleDB

 

Ilimitado

Solicitud

MongoDB

Red social

 

personalizada

RethinkDB

 

 

HBase

 

Accumulo

 

ElasticSearch

 

Solr

 

 

Solicitud

Hadoop

Big data

 

Analítica

Spark

 

 

Parallel DWH

 

Cassandra

 

HBase

 

Riak

 

MongoDB

 

Sabemos que el éxito de las bases de datos SQL también proviene de su aspecto integrador. Una base de datos SQL es ideal cuando hay que compartir los datos a los que acceden varias aplicaciones o varios nodos en una aplicación clusterizada. La base de datos SQL es ideal para compartir estados entre aplicaciones monolíticas. Por otro lado, si un servicio encapsula su base de datos, si la base de datos está dedicada a él, entonces no hay ningún problema de integración. Este es el enfoque de microservicio y este aspecto integrador se está desvaneciendo. Cada servicio es responsable de sus datos. Por lo tanto, la base de datos NoSQL se corresponde con las preocupaciones de un servicio, tiene un lado analítico pronunciado y, a menudo, permite tener un modelo lo más cercano posible a los aspectos empresariales a los que sirve. Con frecuencia, esto da como resultado agregados o un sistema orientado a registros, en el que se registran los cambios en los objetos en lugar de sus estados. Optimizamos el modelo y enmascaramos la complejidad de los datos para responder lo más cerca posible a las necesidades del negocio.

Problemas con el uso de bases de datos NoSQL

El uso de bases de datos NoSQL induce nuevas decisiones y cambios organizativos. Los DBA tradicionales para grandes bases de datos SQL, como Oracle, Sybase u otros, se enfrentan a la relativa inmadurez de las soluciones NoSQL. La falta de herramientas, experiencia y conocimiento en los equipos no es tranquilizadora.

De hecho, las herramientas existen y el know-how también, pero a menudo se consideran caros y desproporcionados respecto a la escala de un Pizza Team.

Las decisiones se toman en los equipos de desarrollo, que posteriormente deben gestionar el seguimiento y las inconsistencias en la producción. En los proyectos estratégicos, NoSQL se utiliza a menudo para tener TTM (Time To Market o Tiempo de disponibilidad en el mercado) muy cortos. Podemos aprovechar la facilidad de desarrollo y gestionar una gran cantidad de datos. La tendencia actual para los nuevos proyectos es usar bases de datos SQL solo en proyectos no vitales y no estratégicos, con un releasing lento. TTM

Estas bases de datos NoSQL se pueden mantener en discos o simplemente en memoria en el caso de cachés de datos. Más sencillas de gestionar y rápidas, abren la posibilidad de un funcionamiento distribuido entre varios servidores, es decir, en clústeres.

El funcionamiento en clúster proporciona una posibilidad de seguridad operativa al tener en cuenta los problemas de fallos, utilizando la redundancia a través del intercambio de datos entre los nodos de los servidores.

En un funcionamiento en modo cloud, es decir, en un clúster compartido, es posible ofrecer bases de datos gestionadas. Las bases de datos se gestionan de forma centralizada, pero con instancias dedicadas a cada proyecto. Por lo tanto, es necesario separar las bases para que todos los proyectos no compartan la misma base física. Se necesita una red ultrarrápida entre la base de datos y las aplicaciones que la utilizan. De lo contrario, la proximidad del código del microservicio cerca de su base de datos pierde todo su significado.

El almacenamiento en caché de datos permite conservar datos clave para no cargarlos varias veces, así como compartir datos entre los nodos en un clúster de servidores. Una caché de datos debe administrar la obsolescencia de los datos que aloja, utilizando para ello un mecanismo de invalidación de caché.

La base de datos más sencilla maneja pares clave/valor, lo que permite búsquedas muy rápidas.

Otros tipos de bases de datos NoSQL manejan fácilmente datos polimórficos para los que la clave de identificación es fija, pero los datos asociados fluctúan a través de campos opcionales.

Limitaciones de la base de datos NoSQL

Las consultas SQL permiten hacer casi todo en una base de datos bien diseñada. SQL es conocido y se aprovecha del uso de herramientas como la definición de las seis formas normales.

En NoSQL, se debe ocupar usted mismo de las relaciones entre los documentos y distribuir los datos en los diferentes nodos de un clúster (sharding).

Además, cuando se trata de almacenamiento físico de datos, cada implementación es diferente. A nivel operativo, es necesario hacer algo específico para cada proveedor de base de datos NoSQL. Las API también difieren. Con NoSQL, estamos más cerca del dominio artesanal si lo comparamos con el dominio bien industrializado del mundo SQL tradicional.

VM6883:64

Spring y NoSQL

Al igual que con otras bases de datos SQL, Spring facilita el uso de bases de datos NoSQL a través de un API.

Hay una multitud de bases de datos NoSQL. He aquí las más populares:

Base

Características

MongoDB MongoDB

Base de datos open source de documentos.

CouchDB CouchDB

Base de datos que utiliza JSON para documentos, JavaScript para consultas MapReduce y la API estándar HTTP.

GemFire GemFire

Una plataforma de administración de datos distribuidos que proporciona escalabilidad dinámica, alto rendimiento y persistencia, similar a la de una base de datos.

Redis Redis

Un servidor de estructura de datos en el que las claves pueden contener cadenas, hashes, listas, conjuntos y conjuntos ordenados.

Cassandra Cassandra

Base de datos que proporciona escalabilidad y alta disponibilidad sin comprometer el rendimiento.

memcached memcached

Sistema open source de alto rendimiento, memoria distribuida y almacenamiento en caché de objetos.

Hazelcast Hazelcast

Plataforma de distribución de datos open source altamente escalable.

HBase HBase

Base de datos Hadoop, un almacén de datos grande, distribuido y escalable.

Mnesia Mnesia

Un sistema de administración de bases de datos distribuidas que tiene propiedades de software en tiempo real.

Neo4j Neo4j

Base de datos de tipo grafo open source de alto rendimiento y calidad profesional.

Caché de datos

Las cachés dan respuesta al problema de mantener los datos en memoria para que estén disponibles sin tener que volver a cargarlos. Las cachés de datos están cerca de las bases de datos NoSQL clave/valor. Spring hace que sea bastante simple tener una caché sencilla y otra más elaborada, basada en GenFire. GenFire

«Solo hay dos problemas complicados en informática: nombrar cosas e invalidar la memoria caché». Famosa cita de Phil Karlton.

1. Caché sencilla

Podemos usar un ResourceBundle para cargar un recurso a fin de almacenar en memoria datos fijos, pero esto no siempre es suficiente. Para simplificar, en el resto de este capítulo diremos que «ocultamos» los datos cuando los hacemos disponibles mediante el uso de una caché de datos.

Spring proporciona un soporte simple para cachear beans gestionados por Spring mediante el uso de la anotación @Cacheable.

Esta anotación indica que el resultado de la llamada a un método (o a todos los métodos de una clase) se puede almacenar en caché. Cada vez que se invoca un método, comprobamos si tenemos en memoria el resultado de una llamada previa para los argumentos dados. También podemos proporcionar una expresión SpEL para calcular la clave a través del atributo key, o una implementación KeyGenerator personalizada que puede anular la predeterminada.

Si no se encuentra ningún valor en la memoria caché para la clave calculada, se invoca al método de destino y el valor devuelto se almacena en la memoria caché asociada. Observe que los tipos de retorno opcionales de Java8+ se manejan automáticamente y su contenido se almacena en la caché si está presente.

Se utiliza junto con la anotación @CacheConfig.

Aplicación de ejemplo Spring boot

En la clase principal, la anotación @EnableCaching habilita el soporte de las anotaciones de caché @Cacheable.

La clase correspondiente a la comunidad en el ejemplo:

@Data 
                public class Comunidad { 
                    private String code; 
                    private String name; 

La interfaz del repositorio:

@Repository 
                public interface ComunidadRepository { 
                    Comunidad getByCode(String code); 
                } 

El repositorio:

@Component 
                public class SimpleComunidadRepository implements 
                ComunidadRepository { 
                    @Override 
                    @Cacheable("comunidades") 
                    public Comunidad getByCode(String regionCode) { 
                        try { 
                            Thread.sleep(2000L); 
                        } catch (InterruptedException e) { 
                            throw new IllegalStateException(e); 
                        } 
                        return new Comunidad(regionCode, 
                simulationBase.get(regionCode)); 
                    } 
                 
                    private Map<String,String> simulationBase = new 
                ConcurrentHashMap<String,String>(); 
                    public SimpleComunidadRepository() { 
                        simulationBase.put("01","COMUNIDAD VALENCIANA"); 
                        simulationBase.put("02","COMUNIDAD DE MADRID"); 
                        simulationBase.put("27","ANDALUCÍA"); 
                        simulationBase.put("44","CANTABRIA"); 
                        simulationBase.put("51","GALICIA"); 
                        //[...] 
                        simulationBase.put("03","ASTURIAS"); 
                        simulationBase.put("974","PAÍS VASCO"); 
                        simulationBase.put("975","MURCIA"); 
                    } 
                } 

La ejecución nos proporciona:

Dos segundos entre cada llamada:

01-03-2018 15:45:05.684 [restartedMain] INFO  
                fr.eni.spring5.Ex1CacheSimple.run - 27->Comunidad{code='27',  
                name='ANDALUCIA'} 01-03-2018 15:45:07.687 [restartedMain] INFO 
                 
                fr.eni.spring5.Ex1CacheSimple.run - 44->Comunidad{code='44',  
                name='CANTABRIA'01-03-2018 15:45:09.687 [ 
                 
                 
                 restartedMain] INFO  
                fr.eni.spring5.Ex1CacheSimple.run - 51->Comunidad{code='51',  
                name='GALICIA'} 

A continuación, una segunda pregunta: casi inmediata

01-03-2018 15:45:09.688 [restartedMain] INFO  
                fr.eni.spring5.Ex1CacheSimple.run - 27->Comunidad{code='27',  
                name='ANDALUCIA'01-03-2018 15:45:09.689 [restartedMain] INFO  
                fr.eni.spring5.Ex1CacheSimple.run - 44->Comunidad{code='44',  
                name='CANTABRIA'01-03-2018 15:45:09.689 [restartedMain] INFO  
                fr.eni.spring5.Ex1CacheSimple.run - 51->Comunidad{code='51',  
                name='GALICIA'}---- 

Ocultar datos con GemFire GemFire

GemFire es el data grid (grid de datos) en memoria, basado en Apache Geode. Ayuda a estabilizar nuestros servicios de datos bajo demanda para cumplir con los requisitos de rendimiento de las aplicaciones en tiempo real. Admite escalado consistente y homogénea en múltiples data centers, entrega datos en tiempo real a millones de usuarios, tiene una arquitectura orientada a eventos y es ideal para microservicios. Sus principales características son baja y predecible latencia, su escalabilidad y su elasticidad, notificación de eventos en tiempo real, alta disponibilidad y continuidad de la actividad. Es duradero y se ejecuta en la nube.

Para el siguiente ejemplo, usamos la configuración embebida (Embedded), que es la más sencilla para hacer una caché distribuida de alta disponibilidad.

Ejemplo de uso

Clase Application:

@ClientCacheApplication(name = "CachingGemFireApplication", 
                logLevel = "error")  
                @EnableGemfireCaching 
                @SuppressWarnings("unused"public class Application { 
                  private static final Logger logger= LoggerFactory.getLogger 
                (Application.class); 
                  public static void main(String[] args) { 
                    SpringApplication.run(Application.class, args); 
                  } 
                 
                  @Bean("Comunidades") 
                  public ClientRegionFactoryBean<String, String> 
                      comunidadesRegion(GemFireCache gemfireCache) { 
                    ClientRegionFactoryBean<String, String> comunidadesRegion = new 
                      ClientRegionFactoryBean<>(); 
                    comunidadesRegion.setCache(gemfireCache); 
                    comunidadesRegion.setClose(false); 
                    comunidadesRegion.setShortcut(ClientRegionShortcut.LOCAL); 
                    return comunidadesRegion; 
                  } 
                  @Bean 
                  ComunidadService comunidadService() { 
                    return new ComunidadService(); 
                  } 
                 
                  @Bean 
                  ApplicationRunner runner() { 
                    return args -> { 
                      Comunidad comunidad; 
                      logger.info("comunidad="+requestComunidad("01")); 
                      logger.info("comunidad="+requestComunidad("02")); 
                      logger.info("comunidad="+requestComunidad("03")); 
                      logger.info("comunidad="+requestComunidad("01")); 
                      logger.info("comunidad="+requestComunidad("02")); 
                      logger.info("comunidad="+requestComunidad("03")); 
                    }; 
                  } 
                  private Comunidad requestComunidad(String code) { 
                   ComunidadService comunidadService = comunidadService(); 
                    Comunidad comunidad=comunidadService.getByCode(code); 
                    return comunidad; 
                  } 
                } 

También obtenemos ganancias de rendimiento en la relectura.

VM6883:64

GemFire como base de datos NoSQL

La clase ApplicationEnableGemfireRepositories:

@ClientCacheApplication(name = "DataGemFireApplication", logLevel = 
                "error")  
                @EnableGemfireRepositories 
                public class ApplicationEnableGemfireRepositories { 
                  public static void main(String[] args) throws IOException { 
                    SpringApplication.run(ApplicationEnableGemfireRepositories.class, 
                args); 
                  } 
                 
                  @Bean("Comunidad") 
                  public ClientRegionFactoryBean<String, Comunidad> 
                      comunidadRegion(GemFireCache gemfireCache) { 
                    ClientRegionFactoryBean<String, Comunidad> comunidadRegion = 
                      new ClientRegionFactoryBean<>(); 
                    comunidadRegion.setCache(gemfireCache); 
                    comunidadRegion.setClose(false); 
                    comunidadRegion.setShortcut(ClientRegionShortcut.LOCAL); 
                    return comunidadRegion; 
                  } 
                 
                  @Bean 
                  ApplicationRunner run(ComunidadRepository comunidadRepository) { 
                    return args -> { 
                      Comunidad galicia = new Comunidad("01",”GALICIA”); 
                      Comunidad asturias = new Comunidad("02",”ASTURIAS”); 
                      Comunidad cantabria = new Comunidad("03","CANTABRIA"); 
                      Comunidad murcia = new 
                             Comunidad("04","MURCIA"); 
                      Comunidad madrid = new 
                             Comunidad("05","MADRID"); 
                      asList(galicia, asturias, cantabria, murcia, 
                               hautesAlpes).forEach(comunidad -> 
                System.out.println("\t" + 
                             comunidad)); 
                      comunidadRepository.save(galicia = new 
                             Comunidad("01",”GALICIA”)); 
                      comunidadRepository.save(asturias = new 
                             Comunidad("02",”ASTURIAS”)); 
                      comunidadRepository.save(cantabria = new 
                             Comunidad("03","CANTABRIA")); 
                      comunidadRepository.save(murcia = new 
                             Comunidad("04","MURCIA")); 
                      comunidadRepository.save(madrid = new 
                             Comunidad("05","MADRID")); 
                      System.out.println("Liste des départements..."); 
                      asList(galicia.getCode(), asturias.getCode(), cantabria.getCode()) 
                             .forEach(code -> System.out.println("\t" + 
                             comunidadRepository.findByCode(code))); 
                    }; 
                  } 
                } 

La clase ComunidadService:

Mantenemos la misma clase que antes.

public class ComunidadService {  
                  @Cacheable("Comunidades") 
                  public Comunidad getByCode(String regionCode) { 
                    try { 
                      Thread.sleep(2000L); 
                    } catch (InterruptedException e) { 
                      throw new IllegalStateException(e); 
                    } 
                  return new Comunidad(regionCode, 
                  simulationBase.get(regionCode)); 
                } 
                 
                private Map<String,String> simulationBase = new 
                  ConcurrentHashMap<String,String>(); 
                  public ComunidadService() { 
                      simulationBase.put("01",”GALICIA”); 
                      simulationBase.put("02",”ASTURIAS”); 
                      simulationBase.put("03","CANTABRIA"); 
                      simulationBase.put("04","MURCIA"); 
                      simulationBase.put("05","MADRID"); 
                      simulationBase.put("06","ARAGÓN"); 
                [...] 
                      simulationBase.put("95","CATALUÑA"); 
                      simulationBase.put("971","VALENCIA"); 
                      simulationBase.put("972","ANDALUCÍA"); 
                      simulationBase.put("973","ESTREMADURA"); 
                      simulationBase.put("974","PAÍS VASCO"); 
                      simulationBase.put("975","CASTILLA Y LEÓN"); 
                  } 
                } 

Vemos buenas ganancias de rendimiento con la memoria caché en los logs.

Redis independiente Redis

1. Uso de Redis para la caché de datos

La clase Comunidad sigue siendo la misma que en el primer ejemplo.

El lanzador:

@SpringBootApplication  
                @EnableCaching 
                public class Ex3CacheRedis implements CommandLineRunner { 
                 
                  private static final Logger LOGGER = 
                LoggerFactory.getLogger(Ex3CacheRedis.class); 
                 
                  public static void main(String[] args) { 
                    SpringApplication.run(Ex3CacheRedis.class, args); 
                  } 
                 
                  @Autowired 
                  private ComunidadRepository comunidadRepository; 
                 
                  @Override 
                  public void run(String... args) throws Exception { 
                    comunidadRepository.cacheEvict(); 
                 
                    LOGGER.info("27->{}", comunidadRepository.getByCode("27")); 
                    LOGGER.info("44->{}", comunidadRepository.getByCode("44")); 
                    LOGGER.info("51->{}", comunidadRepository.getByCode("51")); 
                 
                    LOGGER.info("27->{}", comunidadRepository.getByCode("27")); 
                    LOGGER.info("44->{}", comunidadRepository.getByCode("44")); 
                    LOGGER.info("51->{}", comunidadRepository.getByCode("51")); 
                 
                    comunidadRepository.patch("27","__ANDALUCIA__"); 
                    LOGGER.info("27->{}", comunidadRepository.getByCode("27")); 
                 
                  } 
                } 

En el lanzador, tenemos la llamada al método comunidadRepository.cacheEvict() para borrar la caché. Usamos la caché y después parcheamos uno de los valores: comunidadRepository.patch("27","ANDALUCIA");

La interfaz del repositorio:

@org.springframework.stereotype. Repository 
                public interface ComunidadRepository { Comunidad 
                 getByCode(String code); 
                 void cacheEvict(); 
                 parche void(String s, String andalucia__); 
                } 

Agregamos las firmas de los métodos para borrar la caché y el patch del valor.

El repositorio:

@Component 
                public class SimpleComunidadRepository implements 
                ComunidadRepository { 
                  @Override  
                  @CacheEvict(value = "comCode", allEntries = true) 
                  public void cacheEvict() { 
                  } 
                  @Override  
                  @CachePut(value = "comCode", key = "#comCode") 
                  public void patch(String comCode, String valor) { 
                    simulationBase.put(comCode,valor); 
                  } 
                  @Override  
                  @Cacheable("comCode") 
                  public Comunidad getByCode(String regionCode) { 
                    try { 
                      Thread.sleep(2000L); 
                    } catch (InterruptedException e) { 
                      throw new IllegalStateException(e); 
                    } 
                    return new Comunidad(regionCode, 
                simulationBase.get(regionCode)); 
                  } 
                 
                  private Map<String,String> simulationBase = new 
                ConcurrentHashMap<String,String>(); 
                  public SimpleComunidadRepository() { 
                    simulationBase.put("01","GALICIA"); 
                    simulationBase.put("02","ASTURIAS"); 
                    simulationBase.put("27","CEUTA"); 
                    simulationBase.put("44","MELILLA"); 
                    simulationBase.put("51","CASTILLA-LA MANCHA"); 
                    //[...] 
                    simulationBase.put("03","CANTABRIA"); 
                    simulationBase.put("974","PAÍS VASCO"); 
                    simulationBase.put("975","CASTILLA Y LEÓN"); 
                  } 
                } 

La anotación @CachePut(value = "comCode", key = "#comCode") especifica que queremos invalidar la caché para este valor de clave.

La anotación @CacheEvict(value = "comCode", allEntries = true) especifica que borramos la caché. El nombre de la clave es comCode. Elegimos este nombre para indicar que queremos usar el espacio de caché "comcode" para almacenar en caché las llamadas al método getByCode. Las regiones se almacenan en caché con la clave que coincide con el código de región. 

2. Utilizar Redis para gestionar mensajes

Es posible utilizar Redis para gestionar mensajes en modo de publicación-suscripción, por ejemplo. Un capítulo específico presenta los MOM (Message-Oriented Middleware); aquí abordaremos esta posibilidad para Redis con objeto de tener una visión general de las posibilidades de esta herramienta.

En el siguiente ejemplo se muestra cómo utilizar CountDownLatch (java7+) para implementar un cliente y un servidor de mensajes. La idea es enviar tres mensajes, activar la recepción en otro thread y, una vez recibidos los tres mensajes, devolver el control. Para simplificar el código, usamos mensajes en formato String con el template Redis StringRedisTemplate. Los mensajes llegan en orden aleatorio.

El uso de CountDownLatch nos permite mantener el programa vivo hasta que el contador de uso llegue a 0.

El lanzador:

@SpringBootApplication 
                public class Ex5MensajeriaRedis implements CommandLineRunner { 
                 
                  private static final Logger LOGGER = 
                LoggerFactory.getLogger(Ex5MensajeriaRedis.class); 
                 
                  public static void main(String[] args) { 
                    SpringApplication.run(Ex5MensajeriaRedis.class, args); 
                  } 
                 
                  @Autowired 
                  StringRedisTemplate stringRedisTemplate; 
                 
                  @Autowired  
                  CountDownLatch contador; 
                 
                  @Override 
                  public void run(String... args) throws Exception { 
                    LOGGER.info("Sending message..."); 
                    stringRedisTemplate.convertAndSend("mensajeria""Arranque de  la aplicación"); 
                    stringRedisTemplate.convertAndSend("mensajeria""Ejecución de los procesamientos"); 
                    stringRedisTemplate.convertAndSend("mensajeria""Fin de los procesamientos"); 
                    contador.await(); 
                    System.exit(0); 
                  } 
                 
                 
                  @Bean 
                  StringRedisTemplate template(RedisConnectionFactory 
                connectionFactory) { 
                    return new StringRedisTemplate(connectionFactory); 
                  } 
                 
                  @Bean 
                  MessageListenerAdapter listenerAdapter(ReceptorMensajes 
                ReceptorMensajes) { 
                    return new MessageListenerAdapter(ReceptorMensajes, 
                "recibeMensaje"); 
                  } 
                 
                  @Bean 
                  RedisMessageListenerContainer contenedor(RedisConnectionFactory 
                connectionFactory, MessageListenerAdapter 
                listenerAdapter) { 
                    RedisMessageListenerContainer container = new 
                RedisMessageListenerContainer(); 
                    container.setConnectionFactory(connectionFactory); 
                    container.addMessageListener(listenerAdapter, new 
                PatternTopic("mensajeria")); 
                    return container; 
                  } 
                 
                  @Bean 
                  ReceptorMensajes receptor(CountDownLatch contador) { 
                    return new ReceptorMensajes(contador); 
                  } 
                 
                  @Bean 
                  CountDownLatch contador() { 
                    return new CountDownLatch(3); 
                  } 
                 
                  class ReceptorMensajes { 
                    private final Logger LOGGER = 
                LoggerFactory.getLogger(ReceptorMensajes.class); 
                 
                    private CountDownLatch latch; 
                 
                    @Autowired 
                    public ReceptorMensajes(CountDownLatch latch) { 
                      this.latch = latch; 
                    } 
                 
                    public void recibeMensaje(String message) { 
                      LOGGER.info("Reception du message [{}]",message); 
                      latch.countDown(); 
                    } 
                  } 
                } 

Por lo tanto, es muy sencillo usar Redis para una multitud de usos.

MongoDB MongoDB

MongoDB es un sistema de base de datos orientado a documentos, con licencia AGPL y con datos distribuidos en múltiples servidores. No hay ningún esquema de datos. Los controladores están en Apache y la documentación tiene una licencia Creative Common.

Creado en 2007, hubo que esperar hasta la versión 1.4 de 2010 para poder utilizarlo en producción. Los datos están en formato BSON (JSON binario), guardados como colecciones de objetos JSON de varios niveles. En la base de datos, estos registros pueden ser polimórficos con la única restricción de compartir un campo clave principal, llamado «id». Este índice único permite identificar un documento (registro en la base de datos). Las solicitudes se realizan en JavaScript. 

La base de datos tiene un intérprete de comandos basado en texto directamente accesible a través del binario Mongo. Existen herramientas gráficas gratuitas, como nosqlbooster4mongo.

1. MongoDB con Spring Boot

Utilizamos la versión Community Edition de MongoDB para nuestras pruebas.

Para pruebas unitarias y de integración, también es posible utilizar las pruebas de contenedor (https://www.testcontainers.org/modules/databases/mongodb/).

Para este ejemplo, ponemos en el pom.xml el starter spring-boot-starter-data-mongodb. El efecto de esto es incluir las dependencias de MongoDB. spring-boot-starter-data-mongodb

No hay diferencias notables con un programa típico que usa SQL.

La clase del lanzador:

  
                @SpringBootApplication 
                public class Ex1MongoDB implements CommandLineRunner { 
                 
                  private static final Logger LOGGER = LoggerFactory.getLogger(Ex1MongoDB.class); 
                 
                  @Autowired 
                  private CapitalRepository repository; 
                 
                  public static void main(String[] args) { 
                    SpringApplication.run(Ex1MongoDB.class, args); 
                  } 
                 
                  @Override 
                  public void run(String... args) throws Exception { 
                    repository.deleteAll(); 
                    repository.save(new Capital("Afganistán", "Kabul", "Asia")); 
                    repository.save(new Capital("África del sur ", "Pretoria", "África")); 
                    repository.save(new Capital("Albania", "Tirana", "Europa")); 
                    repository.save(new Capital("Argelia", "Argel", "África")); 
                    repository.save(new Capital("Alemania", "Berlín", "Europa")); 
                    repository.save(new Capital("Andorra", "Andorra la Vella", "Europa")); 
                    repository.save(new Capital("Angola", "Luanda", "África")); 
                    repository.save(new Capital("Antigua y Barbuda", "Saint John", "América")); 
                    repository.save(new Capital("Arabia Saudí", "Riad", "Asia")); 
                    repository.save(new Capital("Argentina", "Buenos Aires", "América")); 
                    repository.save(new Capital("Armenia", "Ereván", "Asia")); 
                    repository.save(new Capital("Australia", "Canberra", "Oceanía")); 
                    repository.save(new Capital("Austria", "Viena", "Europa")); 
                    repository.save(new Capital("Azerbaiyán", "Bakú", "Asia")); 
                 
                    // Buscar todas las capitales 
                    LOGGER.info("Todas las capitales usando findAll():"); 
                    for (Capital capital : repository.findAll()) { 
                        LOGGER.info("Capital:{}", capital); 
                    } 
                 
                    // Buscar capital por país 
                    LOGGER.info("Capital encontrada con findByPais('Alemania):"); 
                    LOGGER.info("Alemania:{}", repository.findByPais("Alemania")); 
                    LOGGER.info("Capital encontrada con findByCapital('Canberra'):"); 
                    LOGGER.info("Canberra:{}", repository.findByCapital("Canberra")); 
                 
                    LOGGER.info("Todas las capitales de Asia con findByContinente('Asia'):"); 
                    for (Capital capital: repository.findByContinente("Asia")) { 
                     LOGGER.info("Capital:{}", capital); 
                 
                      capital.getId(); 
                    } 
                  } 
                } 

El repository:

public interface CapitalRepository extends  
                MongoRepository<Capital, String> { 
                    Capital findByPais(String pais); 
                    Capital findByCapital(String capital); 
                    List<Capital> findByContinente(String continente); 
                } 

La clase Capital:

@Getter 
                @ToString 
                public class Capital { 
                  @Id 
                  private String id; 
                 
                  private String pais; 
                  private String capital; 
                  private String continente; 
                 
                  public Capital() { 
                  } 
                 
                  public Capital(String pais, String capital, String continente) { 
                    this.pais = pais; 
                    this.capital = capital; 
                    this.continente = continente; 
                  } 
                } 

2. MongoDB con una API REST MongoDB

La clase Capital sigue siendo la misma que en el primer ejemplo.

En el siguiente ejemplo se muestra cómo utilizar MongoDB con Spring Boot. Contiene una clase para lanzar la aplicación y crear el juego de pruebas sobre la marcha, un repositorio y una clase de dominio.

La clase del lanzador:

  
                @SpringBootApplication 
                public class Ex2MongoDB implements CommandLineRunner { 
                 
                    private static final Logger LOGGER = 
                LoggerFactory.getLogger(Ex2MongoDB.class); 
                 
                    @Autowired 
                    private CapitalRepository repository; 
                 
                    public static void main(String[] args) { 
                        LOGGER.info("Ejemplo Ex2MongoDB : mongoDB y API Rest"); 
                        SpringApplication.run(Ex2MongoDB.class, args); 
                    } 
                 
                    @Override 
                    public void run(String... args) throws Exception { 
                        repository.deleteAll(); 
                        repository.save(new Capital("Afganistán", "Kabul", "Asia")); 
                        repository.save(new Capital("África del Sur ", "Pretoria", "África")); 
                        repository.save(new Capital("Albania", "Tirana", "Europa")); 
                        repository.save(new Capital("Argelia", "Argel", "África")); 
                        repository.save(new Capital("Alemania", "Berlín", "Europa")); 
                        repository.save(new Capital("Andorra", "Andorra la Vella ", "Europa")); 
                        repository.save(new Capital("Angola", "Luanda", "África")); 
                        repository.save(new Capital("Antigua y Barbuda", "Saint John""América")); 
                        repository.save(new Capital("Arabia Saudí", "Riad", "Asia")); 
                        repository.save(new Capital("Argentina", "Buenos Aires", "América")); 
                        repository.save(new Capital("Armenia", "Ereván", "Asia")); 
                        repository.save(new Capital("Australia", "Canberra", "Oceanía")); 
                        repository.save(new Capital("Austria", "Viena", "Europa")); 
                        repository.save(new Capital("Azerbaiyán", "Bakú", "Asia")); 
                 
                        // Buscar todas las capitales 
                        LOGGER.info("Todas las capitales con findAll():"); 
                        for (Capital capital : repository.findAll()) { 
                            LOGGER.info("Capital:{}", capital); 
                        } 
                 
                        // Buscar capital por país 
                        LOGGER.info("Capital encontrada con findByPais('Alemania'):"); 
                        LOGGER.info("Alemania:{}", repository.findByPais("Alemania")); 
                 
                        LOGGER.info("Capital encontrada con findByCapital('Canberra'):"); 
                        LOGGER.info("Canberra:{}", repository.findByCapital("Canberra")); 
                        LOGGER.info("Todas las capitales de Asia con findByContinent('Asia'):"); 
                        for (Capital capital: repository.findByContinente("Asia")) { 
                            LOGGER.info("Capital:{}", capital); 
                 
                            capital.getId(); 
                        } 
                    } 
                } 

Ahora codificamos el repositorio para el que queremos exponer una API REST sobre el recurso Capital, con un path en la consulta "/capitales".

El repositorio:

@RepositoryRestResource(collectionResourceRel = "capital", path = 
                "capitales"public interface CapitalRepository extends  
                MongoRepository<Capital, String> { 
                    Capital findByPais(@Param("pais") String pais); 
                    Capital findByCapital(@Param("capital") String capital); 
                    List<Capital> findByContinente(@Param("continente"String continente); 

El código:

@RepositoryRestResource(collectionResourceRel = "capital", path = 
                "capitales") 

indica que queremos /capitales como base para la URL y acceder a los datos a través de la API REST.

La parte de prueba es más compleja:

Inicio de la prueba:

@RunWith(SpringRunner.class) 
                @SpringBootTest  
                @AutoConfigureMockMvc 
                public class Ex2MongoDBTests { 
                  @Autowired 
                  private MockMvc mockMvc; 
                  @Autowired 
                  private CapitalRepository capitalRepository; 
                  @Before 
                  public void deleteAllBeforeTests() throws Exception { 
                    capitalRepository.deleteAll(); 
                  } 

Ahora estamos probando el acceso al recurso creado por una consulta POST usando mockMvc. Prueba de la creación del recurso mediante una consulta POST:

  @Test 
                  public void shouldCreateCapital() throws Exception { 
                    mockMvc.perform(post("/capitales").content( 
                          "{\"país\": \"Haití\", \"capital\":\"Puerto Príncipe\", 
                \"continente\":\"América\"}")).andExpect( 
                          status().isCreated()).andExpect( 
                          header().string("Location", containsString("capitales/"))); 
                  } 

Ahora estamos probando lo que se devuelve en la respuesta durante una llamada para crear una lista de capitales. Prueba del contenido de la consulta de creación mediante un POST:

  @Test 
                  public void shouldRetrieveCapital() throws Exception { 
                    MvcResult mvcResult = mockMvc.perform(post("/capitales").content( 
                          "{\"país\": \"Haití\", \"capital\":\"Puerto Príncipe\", 
                \"continente\":\"América\"}")).andExpect( 
                          status().isCreated()).andReturn(); 
                    String location = mvcResult.getResponse().getHeader("Location"); 
                    mockMvc.perform(get(location)).andExpect(status().isOk()).andExpect( 
                          jsonPath("$.pais").value("Haití")).andExpect( 
                          jsonPath("$.capital").value("Puerto Príncipe")).andExpect( 
                          jsonPath("$.continente").value("América")); 
                  } 

Ahora probamos el acceso al recurso en modo lectura mediante una consulta GET usando mockMvc. Queremos todas las capitales. Prueba de la lectura del recurso mediante una consulta GET:

  @Test 
                  public void shouldQueryCapital() throws Exception { 
                    MvcResult mvcResult = mockMvc.perform(post("/capitales").content( 
                          "{\"país\": \"Haití\", \"capital\":\"Puerto Príncipe\", 
                \"continente\":\"América\"}")).andExpect( 
                          status().isCreated()).andReturn(); 
                    mockMvc.perform( 
                          get("/capitales/search/findByContinent?continente={continente}""América")).andExpect( 
                          status().isOk()).andDo(print()).andExpect( 
                          jsonPath("$._embedded.capital[0].pais").value( 
                                 "Haití")); 
                  } 

Ahora probamos la actualización del recurso mediante una consulta POST utilizando mockMvc. Prueba de la actualización del recurso mediante una consulta POST:

  @Test 
                  public void shouldUpdateCapital() throws Exception { 
                    MvcResult mvcResult = mockMvc.perform(post("/capitales").content( 
                          "{\"país\": \"Haití\", \"capital\":\"Puerto Príncipe\", 
                \"continente\":\"América\"}")).andExpect( 
                          status().isCreated()).andReturn(); 
                    String location = mvcResult.getResponse().getHeader("Location"); 
                    mockMvc.perform(put(location).content( 
                          "{\"pais\": \"Haití\", \"capital\":\"Puerto Príncipe\", 
                \"continente\":\"América\"}")).andExpect( 
                          status().isNoContent()); 
                 
                    mockMvc.perform(get(location)).andExpect(status().isOk()).andExpect( 
                          jsonPath("$.pais").value("Haití")).andExpect( 
                          jsonPath("$.capital").value("Puerto Príncipe")).andExpect( 
                          jsonPath("$.continente").value("América")); 
                  } 

Ahora probamos la actualización de una variable de recurso mediante una consulta PATCH usando mockMvc.

  @Test 
                  public void shouldPartiallyUpdateCapital() throws Exception { 
                    MvcResult mvcResult = mockMvc.perform(post("/capitales").content( 
                          "{\"país\": \"Haití\", \"capital\":\"Puerto Príncipe\", 
                \"continente\":\"América\"}")).andExpect( 
                          status().isCreated()).andReturn(); 
                    String location = mvcResult.getResponse().getHeader("Location"); 
                    mockMvc.perform( 
                          patch(location).content("{\"país\": \"HAITÍ\"}")).andExpect( 
                          status().isNoContent()); 
                 
                    mockMvc.perform(get(location)).andExpect(status().isOk()).andExpect( 
                          jsonPath("$.pais").value("HAITÍ")).andExpect( 
                          jsonPath("$.capital").value("Puerto Príncipe")).andExpect( 
                          jsonPath("$.continente").value("América")); 
                  } 

Ahora probamos la eliminación del recurso mediante una consulta DELETE usando mockMvc. Prueba de la eliminación del recurso mediante una consulta DELETE:

  @Test 
                  public void shouldDeleteCapital() throws Exception { 
                    MvcResult mvcResult = mockMvc.perform(post("/capitales").content( 
                          "{\"país\": \"Haití\", \"capital\":\"Puerto Príncipe\", 
                \"continente\":\"América\"}")).andExpect( 
                          status().isCreated()).andReturn(); 
                    String location = mvcResult.getResponse().getHeader("Location"); 
                 
                    mockMvc.perform(delete(location)).andExpect(status().isNoContent()); 
                    mockMvc.perform(get(location)).andExpect(status().isNotFound()); 
                  } 
VM6883:64

Puntos clave

  • Las bases de datos NoSQL sustituyen a las bases de datos SQL en proyectos que requieren un TTM eficaz. TTM

  • Las bases de datos NoSQL son adecuadas para microservicios o el Big Data.

  • Los equipos de desarrollo gestionan generalmente las bases de datos NoSQL en producción.

  • Es fundamental un buen sharding que coincida con el reparto de datos entre servidores.

Introducción Spring Batch

En el mundo de la informática, a menudo separamos el procesamiento en tres familias principales: procesamiento en tiempo real, procesamiento por batchs o lotes y una familia de procesamiento híbrido, que es una composición de los dos primeros, como el streaming, que procesa grandes volúmenes en tiempo real. Streaming

images/cap16_pag1.png

En la actualidad, los batchs son el resultado de programas de migración y modernización de antiguos programas de procesamiento masivo convencional. También se utilizan para el Big Data en aplicaciones modernas. Big Data

En general, intentamos prescindir de batchs y procesamos los datos sobre la marcha o en la aplicación, a través de minibatchs o un programador Spring @Scheduled. En algunos casos, los batchs son inevitables.

En general, el procesamiento por batchs requiere acceso completo y exclusivo a las bases de datos, lo que implica detener el procesamiento con interfaces hombre-máquina. En la era de la globalización, se hace imposible cerrar el acceso a las aplicaciones durante el período de los batchs.

En los primeros días de la informática, el procesamiento a menudo se realizaba con Mainframes. Hay que saber que un Mainframe es un servidor muy grande (los grandes pueden alcanzar los 80 000 MIPS) hiperoptimizado para dos cosas: permite a un gran número de usuarios introducir datos durante el día a través de un terminal para procesarlos en masa durante la noche. Estos Mainframes han perdurado hasta hoy día en muchos clientes. En general, los procesamientos están escritos en COBOL, que en su propio diseño es hipereficiente para hacer estos procesamientos. Los informáticos también han podido utilizar Pacbase de IBM, un generador de código COBOL para optimizar y simplificar aún más estas pantallas y batchs. El diseño de la pantalla de entrada de datos y los batchs es realmente rápido con Pacbase. Pacbase

Los costes operativos de estas máquinas, así como la voluntad de IBM de abandonar Pacbase, han impulsado a muchos clientes a migrar sus batchs a soluciones más rentables, a menudo basadas en Spring Batch para el procesamiento nocturno, y a adoptar soluciones SPA (Single Page Application), junto con microservicios basados en Spring para pantallas (o páginas) durante el día. Pacbase

Las aplicaciones contemporáneas utilizan batchs, principalmente para cargar o exportar datos.

Las aplicaciones de Big Data utilizan batchs, además del streaming, para preparar los datos de modo que se puedan procesar más fácilmente. Se separan, mapean, ordenan por clave y después se reducen para dar un resultado final. Algunos batchs también realizan cálculos. Big Data

Históricamente, las bases de datos no existían. Con posterioridad, los batchs procesaron archivos planos. Estos archivos se organizaron con registros estructurados en línea. Más adelante, evolucionaron con registros de tamaño fijo. Después evolucionaron de nuevo con estructuras secuenciales indexadas.

Incluso hoy en día, cuando el rendimiento es crucial, seguimos utilizando archivos planos o sus derivados como con Kafka, por ejemplo, o trabajamos cargando los archivos en RAM. Por este motivo, algunas veces terminamos con máquinas que tienen mucha RAM. En la actualidad, algunas veces encontramos servidores con 384 GB de RAM. Kafka

Un batch generalmente consta de tres partes. La primera parte lee un gran volumen de datos de una base de datos, archivos o cola de mensajes. La segunda parte es el procesamiento de estos datos y, finalmente, la tercera parte consiste en restaurar los datos procesados en una base de datos, archivos o colas de mensajes.

Estos batchs introducen una serie de problemas, como copias de seguridad periódicas de trabajos, reanudación de trabajos erróneos, paralelización de trabajos, programación de trabajos y administración de transacciones. No podremos cubrir todos los aspectos en este capítulo, pero veremos ejemplos que nos darán una visión de conjunto.

Por lo general, no es posible procesar por batchs durante el día, ya que estos, por lo comun, bloquean las bases de datos y hacen que las cachés de datos no se pueden utilizar. Algunos batchs trabajan sin transacciones unitarias o realizan transacciones en los lotes de procesamiento de datos: por ejemplo, una confirmación cada 5 000 inserciones. En algunos bancos grandes, estos procesamientos por batchs pueden durar más de diez horas. Batch

Los batchs tienen un propósito similar a los microservicios basados en flujo. Los batchs monolíticos son muy raros. Por lo general, se dividen en pequeñas tareas que se suceden generando flujos, con cada tarea consumiendo un flujo y produciendo otro, con ramas que se pueden ejecutar en paralelo con separaciones y agrupaciones de flujo. Al igual que con los microservicios, cada job (o unidad de trabajo) realiza una tarea simple que forma parte de un todo complejo. Job

Generalmente, hay un programador que encadena tareas muy simples:

  • Convertidor de formato de datos: un formato de entrada y un formato de salida.

  • Validación de datos: separación de datos válidos de los erróneos (para reproducir el procesamiento de errores).

  • Procesamiento y cálculos sobre los datos: en salida, varios niveles con agregados de cálculos.

Y operadores en los flujos:

  • Ordenación.

  • Separación de datos en varias partes.

  • Fusión.

images/cap16_pag4.png

Históricamente, cada cliente migraba los batchs a Java a su manera. Como se mencionó, los batchs basados en COBOL son muy rápidos y la ventana de tiempo asignada a su ejecución, generalmente por la noche, a menudo es corta. Se presentaron problemas importantes relacionados con el rendimiento de Java y la planificación de estos batchs.

Spring Batch se desarrolló con Accenture en 2008 para facilitar y armonizar el procesamiento por batchs dentro del framework Spring.

Spring Batch se puede utilizar solo o junto con Spring Integration para aplicaciones Mixed Batch/Integration.

Spring Batch está dirigido principalmente a los batchs de tamaño medio. Para batchs pequeños, solo haremos un programa Java Spring normal y, para batchs muy grandes, recurriremos a soluciones de Big Data, como Hadoop o Spark.

Los ejemplos de este capítulo utilizan Spring Boot por su simplicidad de implementación, pero el comportamiento es el mismo en una aplicación Spring sencilla con, sin embargo, un poco más de configuración. Spring Boot determina también aquí las dependencias y sus versiones de manera óptima en relación con las necesidades.

Arquitectura de un batch

La aplicación Spring Batch se descompone en varias partes. Una parte técnica se codifica en forma de framework configurable, y una parte reservada para el procesamiento específico relacionado con la parte funcional es personalizable.

El batch se compone de un lanzador: el Job launcher, que lanza los trabajos llamados Jobs, compuestos por pasos denominados Steps. Todo se traza en una base de datos a través del Repository.

images/16EP03N.png

Los listeners también permiten la llamada de código a través de las notificaciones sobre el ciclo de vida de los trabajos y las etapas.

Cada etapa (Job) se divide en tres partes:

images/cap16_pag6.png

El Reader, que lee los datos denominados Items como entrada; el Processor, que procesa estos datos, y el Writer, que escribe los datos como salida.

Ejemplo de Spring Batch versión 4

Ahora codificamos un ejemplo de un pequeño batch para ilustrar cómo funciona Spring Batch.

Dependencia Maven:

<dependencies> 
                   <dependency> 
                       <groupId>org.springframework.batch</groupId> 
                       <artifactId>spring-batch-core</artifactId> 
                       <version>4.3.5.RELEASE</version> 
                   </dependency> 
                  <dependency> 
                    <groupId>org.springframework.boot</groupId> 
                    <artifactId>spring-boot-starter-batch</artifactId> 
                  </dependency> 
                </dependencies> 

Vamos a crear una clase main para ejecutar el batch:

@EnableBatchProcessing 
                @SpringBootApplication 
                public class Main { 
                  public static void main(String [] args) { 
                    System.exit(SpringApplication.exit(SpringApplication.run( 
                        BatchConfiguration.class, args))); 
                  } 
                } 

Vamos a crear una clase de configuración para configurar el batch con un job que contiene un Step. Un Job es un conjunto de Steps y cada uno realiza una operación elemental. Job

@Configuration 
                public class BatchConfiguration { 
                  @Autowired 
                  private JobBuilderFactory jobBuilderFactory; //Versión 4 
                  @Autowired 
                  private StepBuilderFactory stepBuilderFactory; //Versión 4 
                  @Bean 
                  public Step step1() { 
                    return stepBuilderFactory.get("step1") 
                        .tasklet(new Tasklet() { 
                          public RepeatStatus execute(StepContribution 
                contribution, ChunkContext chunkContext) { 
                            return null; 
                          } 
                        }) 
                        .build(); 
                  } 
                  @Bean 
                  public Job job(Step step1) throws Exception { 
                    return jobBuilderFactory.get("job1") 
                        .incrementer(new RunIdIncrementer()) 
                        .start(step1) 
                        .build(); 
                  } 
                } 

Cargador H2 desde un CSV

Codificamos un segundo ejemplo que muestra cómo cargar un archivo CSV en una base de datos H2. El ejemplo utiliza un Reader (unidad de lectura) para acceder al contenido del archivo CSV. Utiliza el FlatFileItemReader, para el que indicamos el formato de una línea a través de la declaración: FlatFileItemReader<Comunidad> reader = newFlatFile ItemReader<Comunidad>();. La unidad de lectura utiliza un LineMapper para descodificar una línea en el archivo CSV. Tenemos un Writter para escribir en la base de datos H2. Se trata de un JdbcBatchItemWriter. Almacena una fila de datos en H2. Tenemos un ItemProcessor que procesa una fila de datos. Aquí, nuestro procesador pone en mayúsculas la información que lee del archivo CSV. Tenemos un Job que encadena etapas llamadas Step. Nuestro Step step1 lee la línea en el CSV, la procesa con el processor y la guarda en H2.

El lanzador:

@SpringBootApplication 
                public class Application { 
                 
                  public static void main(String[] args) throws Exception { 
                    SpringApplication.run(Application.class, args); 
                  } 
                } 

La clase Comunidad:

@Data 
                @ToString 
                @AllArgsConstructor 
                @NoArgsConstructor 
                public class Comunidad { 
                  private String codigo; 
                  private String nombre; 
                } 

Configuración por batchs:

@Configuration 
                @EnableBatchProcessing 
                public class BatchConfiguration { 
                 
                  @Autowired 
                  public JobBuilderFactory jobBuilderFactory; 
                 
                  @Autowired 
                  public StepBuilderFactory stepBuilderFactory; 
                 
                  @Autowired 
                  public DataSource dataSource; 
                 
                  // tag::readerwriterprocessor[] 
                  @Bean 
                  public FlatFileItemReader<Comunidad> reader() {  FlatFileItemReader
                    FlatFileItemReader<Comunidad> reader = new 
                        FlatFileItemReader<Comunidad>(); 
                    reader.setResource(new ClassPathResource("comunidades.csv")); 
                    reader.setLineMapper(new DefaultLineMapper<Comunidad>() {{ 
                      setLineTokenizer(new DelimitedLineTokenizer() {{ 
                        setNames(new String[]{"codigo", "nombre"}); 
                      }}); 
                      setFieldSetMapper(new BeanWrapperFieldSetMapper<Comunidad>() {{ 
                        setTargetType(Comunidad.class); 
                      }}); 
                    }}); 
                    return reader; 
                  } 
                 
                  @Bean 
                  public ComunidadItemProcessor processor() { 
                    return new ComunidadItemProcessor(); 
                  } 
                 
                  @Bean 
                  public JdbcBatchItemWriter<Comunidad> writer() { 
                    JdbcBatchItemWriter<Comunidad> writer = new 
                JdbcBatchItemWriter<Comunidad>(); 
                    writer.setItemSqlParameterSourceProvider(new 
                        BeanPropertyItemSqlParameterSourceProvider<Comunidad>()); 
                        writer.setSql("INSERT INTO comunidad (codigo, nombre) VALUES 
                        (:codigo, :nombre)"); 
                    writer.setDataSource(dataSource); 
                    return writer; 
                  } 
                 
                  @Bean 
                  public Job importUserJob(JobCompletionNotificationListener listener) { 
                    return jobBuilderFactory.get("importUserJob") 
                          .incrementer(new RunIdIncrementer()) 
                          .listener(listener) 
                          .flow(step1()) 
                          .end() 
                          .build(); 
                  } 
                 
                  @Bean 
                  public Step step1() { 
                    return stepBuilderFactory.get("step1") 
                          .<Comunidad, Comunidad>chunk(10) 
                          .reader(reader()) 
                          .processor(processor()) 
                          .writer(writer()) 
                          .build(); 
                  } 
                 
                } 

El ItemProcessor:

public class ComunidadItemProcessor implements  
                ItemProcessor<Comunidad, Comunidad> { 
                 
                  private static final Logger log = 
                LoggerFactory.getLogger(ComunidadItemProcessor.class); 
                 
                  @Override 
                  public Comunidad process(final Comunidad comunidad) 
                    throws Exception { 
                    final String codigo= comunidad.getCodigo().toUpperCase(); 
                    final String nombre = comunidad.getNombre().toUpperCase(); 
                    final Comunidad transformedComunidad = new 
                Comunidad(codigo, nombre); 
                    log.info("Converting (" + comunidad + ") into 
                       (" + transformedComunidad + ")"); 
                 
                    return transformedComunidad; 
                  } 
                } 

El listener de fin de batch:

@Component 
                public class JobCompletionNotificationListener extends  
                JobExecutionListenerSupport { 
                 
                  private static final Logger log = 
                    LoggerFactory.getLogger(JobCompletionNotificationListener.class); 
                 
                  private final JdbcTemplate jdbcTemplate; 
                 
                  @Autowired 
                  public JobCompletionNotificationListener(JdbcTemplate jdbcTemplate) { 
                    this.jdbcTemplate = jdbcTemplate; 
                  } 
                 
                  @Override 
                  public void afterJob(JobExecution jobExecution) { 
                    if (jobExecution.getStatus() == BatchStatus.COMPLETED) { 
                      log.info("Fin del job. Resultado para verificación:"); 
                 
                      List<Comunidad> results = jdbcTemplate.query("SELECT code, 
                           nombre FROM comunidad", new RowMapper<Comunidad>() { 
                        @Override 
                        public Comunidad mapRow(ResultSet rs, int row) throws 
                          SQLException { 
                          return new Comunidad(rs.getString(1), rs.getString(2)); 
                        } 
                      }); 
                 
                      for (Comunidad comunidad : results) { 
                        log.info("[" + comunidad + "] en base."); 
                      } 
                 
                    } 
                  } 
                } 

El batch crea las tablas en la base de datos del Repository para los datos y el procesamiento del batch.

Tablas

Contenido

BATCH_JOB_EXECUTION

Información sobre lanzamientos de batchs con estado.

BATCH_JOB_EXECUTION_CONTEXT

El contexto.

BATCH_JOB_EXECUTION_PARAMS

Los argumentos.

BATCH_JOB_INSTANCE

Las instancias.

BATCH_STEP_EXECUTION

Las etapas.

BATCH_STEP_EXECUTION_CONTEXT

Los contextos de ejecución de las etapas.

COMUNIDAD

Los datos procesados.

Dependencias Spring Batch 3 y 4

Spring Batch 4 ha revisado todas sus dependencias respecto a Spring Batch 3 y requiere Java 8+ y Spring 5+.

Otras novedades de la versión 4

Hay builders para ItemReader, ItemProcessor y ItemWriter, así como una variedad de builders para simplificar la escritura de batchs cuyo nombre informa sobre el contenido: ItemReader ItemProcessor ItemWriter

Elemento

Builder

AmqpItemReader

AmqpItemReaderBuilder

ClassifierCompositeItemProcessor

ClassifierCompositeItemProcessorBuilder

ClassifierCompositeItemWriter

ClassifiercompositeItemWriterBuilder

CompositeItemWriter

CompositeItemWriterBuilder

FlatFileItemReader

FlatFileItemReaderBuilder

FlatFileItemWriter

FlatFileItemWriterBuilder

GemfireItemWriter

GemfireItemWriterBuilder

HibernateCursorItemReader

HibernateCursorItemReaderBuilder

HibernateItemWriter

HibernateItemWriterBuilder

HibernatePagingItemReader

HibernatePagingItemReaderBuilder

JdbcBatchItemWriter

JdbcBatchItemWriterBuilder

JdbcCursorItemReader

JdbcCursorItemReaderBuilder

JdbcPagingItemReader

JdbcPagingItemReaderBuilder

JmsItemReader

JmsItemReaderBuilder

JmsItemWriter

JmsItemWriterBuilder

JpaPagingItemReader

JpaPagingItemReaderBuilder

MongoItemReader

MongoItemReaderBuilder

MultiResourceItemReader

MultiResourceItemReaderBuilder

MultiResourceItemWriter

MultiResourceItemWriterBuilder

Neo4jItemWriter

Neo4jItemWriterBuilder

RepositoryItemReader

RepositoryItemReaderBuilder

RepositoryItemWriter

RepositoryItemWriterBuilder

ScriptItemProcessor

ScriptItemProcessorBuilder

SimpleMailMessageItemWriter

SimpleMailMessageItemWriterBuilder

SingleItemPeekableItemReader

SingleItemPeekableItemReaderBuilder

StaxEventItemReader

StaxEventItemReaderBuilder

StaxEventItemWriter

StaxEventItemWriterBuilder

SynchronizedItemStreamReader

SynchronizedItemStreamReaderBuilder

Evoluciones de los batchs

No entraremos en detalles, pero el mundo del batch se está moviendo hacia la gestión de procesos externos para poder utilizar las ventajas del cloud, con Spring Batch centrado en el uso de una sola máquina.

Por lo tanto, utilizaremos Spring Cloud Data Flow con Spring Boot Task y Spring Cloud Function.

Spring Cloud Function permite ejecutar funciones bajo demanda a través del equivalente de los lambdas AWS. Spring Boot Task permite gestionar la planificación de tareas y Spring Cloud Data Flow integra la noción de flujo en la ecuación. Esto agrega un nivel de abstracción y es posible planificar un conjunto mucho más grande:

images/EP1601.png

Puntos clave

  • Spring Batch es accesible incluso si tiene poca experiencia en este campo porque los problemas técnicos se gestionan de forma automática.

  • Spring Batch permite convertir fácilmente datos de un formato a otro.

  • En la medida de lo posible, intentaremos procesar los datos en tiempo real para minimizar el número de batchs.

VM6883:64

Introducción

Los Middleware de mensajes, Message-Oriented Middleware en inglés, o su abreviatura MOM, es un software que intercambia información con mensajes a través de una red informática. Esto permite tener un acoplamiento débil y asíncrono a través del almacenamiento de mensajes a la espera de ser procesados. Los mensajes se componen de una parte técnica utilizada por la parte middleware y una parte de datos, cuyo formato queda a discreción de las aplicaciones que utilizan estos mensajes. Message-Oriented Middleware MOM

También hablamos de Brokers y servidores de mensajes. Brokers

Existen dos familias principales:

  • Los mensajes pueden ser enrutados, enriquecidos, empobrecidos, acoplados, transformados por el middleware, como para los servicios web, a menudo denominados integración de aplicaciones empresariales o abreviado como EAI (Enterprise Application Integration en inglés). En este caso, el middleware se utiliza para permitir el intercambio de información entre aplicaciones heterogéneas con tendencias monolíticas. EAI Enterprise Application Integration

  • Los mensajes simplemente se transportan (y almacenan temporalmente) sin modificaciones. A estos se les llama mensajes pobres.

Al igual que sucede con los servicios web, que suelen ser síncronos, la tendencia es utilizar mensajes pobres y no poner reglas de negocio en la gestión de mensajes. Hay una excepción a esta regla, como veremos en este capítulo. En el caso de las aplicaciones clusterizadas utilizamos algunas particularidades «de negocio» para facilitar la redundancia y la persistencia de los mensajes, de forma parecida al particionamiento de una base de datos clusterizada.

Por lo tanto, el middleware de mensajes se utiliza para intercambiar datos asíncronos entre softwares. Cada implementación adopta un punto de vista que aporta optimizaciones respecto a sus casos de uso. Algunas veces usamos varios MOM para la misma aplicación compuesta y para un IS en su conjunto, y algunos mensajes incluso se convierten de un MOM a otro entre dos aplicaciones.

Hay dos formas de intercambiar mensajes entre un editor (publisher) y un consumidor (consumer) de mensajes, de forma parecida a lo que sucede con los correos electrónicos:

  • El modo «publicación y suscripción»: como sucede para una lista de difusión, una suscripción permite a un consumidor suscribirse a un tema (lista de difusión para correos electrónicos) y leer los mensajes. Una vez que todos los suscriptores han leído la publicación, puede o no ser eliminada del almacenamiento. Publicación y suscripción

  • El modo punto a punto: al igual que sucede con los correos electrónicos de un solo destinatario, un envío punto a punto permite enviar un mensaje de un punto a otro, almacenándolo en una cola. Es posible eliminar el mensaje cuando se haya leído. Punto a punto

Vemos aquí que, dependiendo del caso, el mensaje se puede borrar o no, que su borrado se puede diferir en el tiempo, que algunas veces es necesario un mecanismo que notifique al emisor que su mensaje se ha leído, que los mensajes se deben almacenar de forma permanente y segura, que podemos tener un gran volumen de almacenamiento, que el mensaje va a aprovechar todo el ancho de banda de la red y que será necesario serializar (a veces decimos plegar) el mensaje para su transporte y deserializarlo (desplegarlo), con lo que tendrá que ser cifrado. También es necesario administrar los diferentes formatos de mensaje si las aplicaciones evolucionan entre el momento en el que se entrega el mensaje y en el que se lee.

Los mensajes están en formato de texto o binario y están acoplados a un esquema que los describe, esquema que algunas veces se puede versionar.

Así que disponemos de varias opciones de implementación o topologías. Inicialmente, con Java, tenemos dos protocolos principales:

Protocolo

Nombre

Peculiaridades

AMQP

Advanced Message Queuing Protocol

apertura y fiabilidad

MQTT

Message Queuing Telemetry Transport

compacidad

Hay implementaciones estándar, open source o propietarias de las que aportamos algunos ejemplos.

VM6883:64

Implementaciones open source

Protocolo

Nombre del proveedor

ActiveMQ ActiveMQ

Apache Software Foundation

HornetMQ

JBoss

JBoss Messaging

JBoss

Kafka Kafka

Apache Software Foundation

RabbitMQ RabbitMQ

AMQP

ZeroMQ

Imatix

Estas implementaciones son gratuitas, pero el soporte y las adaptaciones se deben hacer internamente y, si encuentra un problema, debe gestionarlo. Son sencillas en cuanto al desarrollo, pero se pueden convertir rápidamente en un infierno durante la producción y el balanceo de carga. Por este motivo, las empresas se ofrecen a ayudar mejorando las implementaciones y ofreciendo soporte a nivel del desarrollador y la puesta en producción. Incluso es posible personalizar la implementación para adaptarla a las necesidades del cliente, optimizando ciertos aspectos en función del uso. Una breve intervención de un experto en el producto, aunque a menudo se percibe como costosa, generalmente ahorra un tiempo considerable y anticipa problemas.

Un experto explicará, por ejemplo, el mecanismo de los retry que requieren tener sistemas idempotentes en caso de timeout, dando como resultado un retry con todo lo que implica. No es suficiente ver vídeos de kárate para tener un cinturón negro en kárate. No basta con seguir un tutorial para elegir una arquitectura empresarial, cuya implementación durará años. Hay que practicar, formarse, consultar a expertos y experimentar.

VM6883:64

Implementaciones propietarias

Protocolo

Nombre del proveedor

IBM WebSphere MQ

IBM

MSMQ

Tibco Software

TIBCO Rendezvous

JBoss

Synchrony Messaging

Axway

A menudo, estas implementaciones se combinan con otras herramientas en un conjunto coherente. Con frecuencia son costosas, pero hay soporte y garantías. También proporcionan soluciones listas para usar y, algunas veces, están muy bien adaptadas a ciertos contextos.

VM6883:64

Casos de uso

Hoy en día, existen multitud de protocolos y API, como JMS, Stomp, MQTT, AMQP, XMPP, etc., y herramientas basadas en mensajes, como Flume, Logstash, Syslog, etc.

En este capítulo, veremos algunos ejemplos de uso con Spring para ActiveMQ (JMS), Redis y RabbitMQ para un uso puro MOM abordando el contexto de uso. ActiveMQ

ActiveMQ es la respuesta por defecto para hacer MOM. Las herramientas Kafka y RabbitMG tienen diferentes patterns de arquitectura para abordar los problemas de envío y recepción de mensajes. Kafka

Para el desarrollo, debemos elegir la implementación en función del contexto de uso. Algunas veces aún no conocemos todas las limitaciones futuras y no sabemos cómo evolucionará la aplicación. Por lo tanto, es necesario separar en el código la implementación de la entrega y el consumo de los mensajes del código de negocio, para que sea relativamente fácil cambiar de MOM. Este es el enfoque de la arquitectura hexagonal. Para la parte técnica del código, nos adaptamos lo mejor posible al MOM utilizado y Spring ofrece mucha ayuda en este punto con sus capas de abstracción.

De hecho, migraremos la aplicación para tener en cuenta las preocupaciones más importantes:

  • facilidad de uso a nivel de codificación,

  • facilidad para las pruebas,

  • escalado (scaling) horizontal y vertical,

  • seguridad de funcionamiento (tiempo de inactividad, recuperación de errores), 

  • velocidad del procesamiento.

En un principio, para un MVP (Minimal Viable product o Producto Mínimo Viable), codificamos de la manera más simple posible y, posteriormente, nos volvemos más complejos cuando se balancea la carga.

En el resto de este capítulo, nos centramos en la parte técnica de la aplicación que define cómo publicamos y escuchamos mensajes. Veremos cómo se transportan y almacenan. Esta parte se puede implementar por un equipo dedicado de arquitectos o por los equipos de desarrollo, dependiendo del grado deseado de optimización para optimizar los esfuerzos. Un equipo solo podrá ir más allá algunas veces en la optimización respecto a sus casos de uso, pero su trabajo no se puede adaptar fácilmente a otro equipo. Por el contrario, un equipo de arquitectos tenderá a uniformizar o suavizar las necesidades para tratar los casos principales. Hay que encontrar un equilibrio.

MOM se utiliza para un conjunto de aplicaciones relacionadas con uno o más productos. En el caso de que dividamos el Sistema de Información (SI) en componentes técnicos o productos, vemos, como sucede en el caso de los servicios web, que los servicios se consumen y después es necesario acordar los formatos, las versiones, y que estos esquemas forman parte del ciclo de evolución del SI.

De la misma manera, debemos tener en cuenta las cuestiones de explotación, ya sea durante la instalación de entornos o a nivel del uso diario de las aplicaciones.

Las posibles evoluciones agrupadas por ejes son las siguientes:

Eje de arquitectura:

  • aplicación monolítica (llamadas entre servicios directos internamente),

  • microservicio.

Eje de comunicación:

  • aplicación con un bus de empresa SOA,

  • aplicación con un bus de empresa WS deficiente,

  • aplicación con MOM tipo JMS,

  • aplicación con MOM tipo rápido.

Eje de programación:

  • aplicación de programación imperativa,

  • aplicación de programación responsive.

Eje de la base de datos:

  • SQL,

  • NoSQL.

Eje del modelo de datos:

  • relacional,

  • basado en eventos de modificación (Event Sourcing).

Eje de tipo de datos:

  • mutable,

  • no mutable.

Con cada evolución, puede ser interesante cambiar de MOM para adoptar el más adecuado. Utilizar Spring hace que sea relativamente fácil cambiar de una implementación a otra si desde el principio hemos decidido separar la parte técnica de la funcional. Además, este principio de codificación se puede aplicar a cualquier tipo de programa.

También es posible utilizar un MOM como base de datos con un enfoque de Event Sourcing, que es un pattern de arquitectura. Propone centrarse en la secuencia de cambios de estado de una aplicación. A partir de esta secuencia, podemos determinar el último estado de esta aplicación reproduciendo las modificaciones del estado inicial. Por ejemplo, para una cuenta bancaria, registramos entradas y salidas que son eventos y podemos calcular el saldo en cualquier momento. A primera vista, esta solución puede parecer ir más allá de las preocupaciones necesarias y suficientes (overkill), pero en la práctica es muy interesante en términos de diagnóstico, recuperación de errores, reporting, etc.

JMS y ActiveMQ

JMS (Java Messaging Service) es una interfaz de programación que permite intercambiar mensajes de forma asíncrona (y menos habitualmente de manera síncrona en modo punto a punto) entre aplicaciones Java.

Todos los servidores Jakarta EE proporcionan un servicio JMS vinculado a JCA (Java Connector Architecture).

JMS se basa en un middleware para la implementación.

Históricamente, hay varias versiones de JMS:

Versiones

Fechas

Adds

1.0.2b

Junio de 2001

 

1.1

Marzo de 2002

 

2.0

Marzo de 2013

JSR 343

2.1

Septiembre de 2017

JSR 368 retirado

JSR 343: JMS (Java Message Service) 2.0

JSR 368: JavaTM Message Service 2.1

JMS ya no es una prioridad para Java EE 8.

Necesita un proveedor de servicios para JMS.

A continuación, se muestra una lista de proveedores open source:

Producto

Proveedor

ActiveMQ

Apache

OpenJMS

Collectif

JBoss Messaging

JBoss

HornetQ

JBoss

JORAM/OW2

ObjectWeb

Open Message Queue

Sun Microsystem

También hay implementaciones propietarias, como BEA Weblogic y Oracle AQ de Oracle, WebSphere MQ de IBM y SAP NetWeaver de SAP, que son útiles en contextos de uso adaptados.

Para los ejemplos que ilustran JMS, usaremos ActiveMQ (http://activemq.apache.org/), de Apache, que soporta JMS desde la versión 1.1 y J2EE 1.4. ActiveMQ

La versión 5.17.0 de Active MQ se lanzó en marzo de 2021. Esta versión también soporta AMQP v1.0 y MQTT v3.1. Funciona con Spring 5.x, Log4j 2.x, JDK 11+. Active MQ se puede montar en la memoria para las pruebas JUnit. Su utilización con Spring es muy sencilla, si se usan las clases JmsInvokerServiceExporter y JmsInvokerProxyFactoryBean.

Utilización simplificada

He aquí un ejemplo sencillo de cómo usar ActiveMQ.

Utilizamos un CountDownLatch para enviar y recibir tres mensajes. Esta clase permite trabajar con la sincronización de varios threads en la situación en la que uno o más threads necesitan esperar a que finalicen uno o más threads diferentes.

A través de su constructor, especificamos el recuento de threads que hay que ejecutar.

Existe un método que permite decrementar este contador. Cuando llegue a cero, se reanudará un thread que estaba pendiente.

El lanzador:

@SpringBootApplication  
                @EnableJms 
                public class Application implements CommandLineRunner { 
                  private static final Logger LOGGER = 
                LoggerFactory.getLogger(Application.class); 
                 
                  @Bean 
                  CountDownLatch contador() { 
                    return new CountDownLatch(3); 
                  } 
                 
                  @Autowired 
                  CountDownLatch contador; 
                 
                  @Bean 
                  public JmsListenerContainerFactory<?> 
                myFactory(ConnectionFactory connectionFactory, 
                    DefaultJmsListenerContainerFactoryConfigurer configurer) { 
                    DefaultJmsListenerContainerFactory factory = new 
                DefaultJmsListenerContainerFactory(); 
                    configurer.configure(factory, connectionFactory); 
                    return factory; 
                  } 
                 
                  @Bean 
                  public MessageConverter jacksonJmsMessageConverter() { 
                    MappingJackson2MessageConverter converter = new 
                MappingJackson2MessageConverter(); 
                    converter.setTargetType(MessageType.TEXT); 
                    converter.setTypeIdPropertyName("_type"); 
                    return converter; 
                  } 
                 
                  public static void main(String[] args) { 
                    ConfigurableApplicationContext context = 
                SpringApplication.run(Application.class, args); 
                  } 
                 
                  @Autowired 
                  JmsTemplate jmsTemplate; 
                 
                  @Override 
                  public void run(String... args) throws Exception { 
                    LOGGER.info("Envío de un mensaje."); 
                    jmsTemplate.convertAndSend("mailbox", new 
                Email("toto@titi.com", "Cuerpo del mensaje 1")); 
                    LOGGER.info("Envío de un mensaje."); 
                    jmsTemplate.convertAndSend("mailbox", new 
                Email("tata@tutu.com", "Cuerpo del mensaje 2")); 
                    LOGGER.info("Envío de un mensaje."); 
                    jmsTemplate.convertAndSend("mailbox", new 
                Email("tete@tyty.com", "Cuerpo del mensaje 3")); 
                    LOGGER.info("Espera a que termine el procesamiento..."); 
                    contador.await(); 
                    System.exit(0); 
                  } 
                } 

En nuestro ejemplo se intercambian los objetos Email para demostrar el uso de ActiveMQ.

La clase Email:

@Data 
                @ToString 
                @AllArgsConstructor 
                @NoArgsConstructor 
                public class Email { 
                    private String to; 
                    private String body; 
                } 

Codificamos una clase que se encargará de recibir y procesar los mensajes recibidos. 

La clase Receiver:

@Component 
                public class Receiver { 
                  private static final Logger LOGGER = 
                LoggerFactory.getLogger(Receiver.class); 
                 
                  private CountDownLatch latch; 
                 
                  //Constructor 
                  public Receiver(CountDownLatch latch) { 
                    this.latch = latch; 
                  } 
                 
                  @JmsListener(destination = "mailbox", containerFactory = 
                "myFactory") 
                  public void receiveMessage(Email email) { 
                    LOGGER.info("Recepción[{}]",email); 
                 
                    latch.countDown(); 
                  } 
                } 

El ejemplo es sencillo y muestra cómo enviar y recibir mensajes.

RabbitMQ RabbitMQ

RabbitMQ se basa en AMQP y fue creado por Pivotal. Tiene licencia Mozilla Public License. Toma el relevo de ActiveMQ.

1. Spring AMQP y RabbitMQ

En el mundo de los intercambios de mensajes asíncronos, el lado del productor y del consumidor están desacoplados. Un productor produce un mensaje en un área lógica llamada Exchange. Publicamos un mensaje a través de la routing key. La cola está determinada por esta routing key, pero el productor no sabe qué cola se utilizará para transmitir su mensaje.

El consumidor declara una cola, se inscribe respecto a un exchange y define una binding key que indica la regla de enrutamiento que permite que el mensaje se le reenvíe.

Esto permite filtrar los mensajes que desea recibir en el área Exchange.

Los intercambios son de dos tipos:

  • Fanout: enviamos mensajes en broadcast sin reglas de enrutamiento o routing.

  • Direct: correspondencia entre la routing key y la binding key.

  • Topic: para una routing key, posibilidad de utilizar comodines (wildcards) en la binding key.

Podemos modelar de la siguiente manera:

Elemento

Pattern

Observaciones

Exchange

Business domain

 

Queue

service

Consume los mensajes

Routing key

Tipo de evento enviado

 

Binding key

Agregador de eventos para un servicio

 

En una aplicación de microservicio, por ejemplo, se separan las funcionalidades y los eventos se propagan a través de diferentes «nodos» de aplicación. Vemos que los mensajes están muy orientados al streaming de mensajes.

Si separamos las funcionalidades, podemos escalar las partes que lo solicitan independientemente unas de otras, sin que importe dónde se produzcan o consuman los mensajes, siempre y cuando los productores se comuniquen con los consumidores.

También podemos tener partes que sean en tiempo real y otras que funcionen en modo batch. Asimismo, vemos que no hay nada que impida el bucle en el sistema.

Para los administradores de mensajes tradicionales, una buena cola de mensajes es una cola de mensajes vacía. Se hace todo lo posible para mantener las colas de mensajes lo más pequeñas posible.

2. Ejemplo de RabbitMQ

Para este ejemplo, necesita un RabbitMQ que funcione. Podemos instalarlo desde https://www.rabbitmq.com/ o ejecutarlo en un docker.

Para ejecutarlo en un docker, simplemente use el comando: docker-compose up en un directorio que contenga este archivo:

rabbitmq: 
                  image: rabbitmq:management 
                  ports: 
                    - "5672:5672" 
                    - "15672:15672" 

El lanzador de la aplicación:

@SpringBootApplication 
                public class Application implements CommandLineRunner { 
                  private static final Logger LOGGER = 
                LoggerFactory.getLogger(Application.class); 
                 
                  final static String queueName = "spring-boot"; 
                 
                  @Bean 
                  Queue queue() { 
                    return new Queue(queueName, false); 
                  } 
                 
                  @Bean 
                  TopicExchange exchange() { 
                    return new TopicExchange("spring-boot-exchange"); 
                  } 
                 
                  @Bean 
                  Binding binding(Queue queue, TopicExchange exchange) { 
                    return BindingBuilder.bind(queue).to(exchange).with(queueName); 
                  } 
                 
                  @Bean 
                  SimpleMessageListenerContainer container(ConnectionFactory 
                connectionFactory, 
                                           MessageListenerAdapter listenerAdapter) { 
                    SimpleMessageListenerContainer container = new 
                SimpleMessageListenerContainer(); 
                    container.setConnectionFactory(connectionFactory); 
                    container.setQueueNames(queueName); 
                    container.setMessageListener(listenerAdapter); 
                    return container; 
                  } 
                 
                  @Bean 
                  MessageListenerAdapter listenerAdapter(Receiver receiver) { 
                    return new MessageListenerAdapter(receiver, "receiveMessage"); 
                  } 
                 
                  public static void main(String[] args) throws InterruptedException { 
                    SpringApplication.run(Application.class, args); 
                  } 
                 
                  @Autowired 
                  private RabbitTemplate rabbitTemplate; 
                 
                  @Autowired 
                  private Receiver receiver; 
                 
                  @Autowired 
                  private ConfigurableApplicationContext context; 
                 
                  @Override 
                  public void run(String... args) throws Exception { 
                    LOGGER.info("Envío de un mensaje."); 
                    rabbitTemplate.convertAndSend(Application.queueName, 
                "Mensaje número 1"); 
                 
                    LOGGER.info("Envío de un mensaje."); 
                    rabbitTemplate.convertAndSend(Application.queueName, 
                " Mensaje número 2"); 
                 
                    LOGGER.info("Envío de un mensaje."); 
                    rabbitTemplate.convertAndSend(Application.queueName, 
                " Mensaje número 3"); 
                 
                    receiver.getLatch().await(10000, TimeUnit.MILLISECONDS); 
                    context.close(); 
                  } 
                } 

El receptor del mensaje:

@Component 
                public class Receiver { 
                  private static final Logger LOGGER = 
                LoggerFactory.getLogger(Receiver.class); 
                 
                  private CountDownLatch latch = new CountDownLatch(3); 
                 
                  public void receiveMessage(String email) { 
                    LOGGER.info("Recepción[{}]",email); 
                    latch.countDown(); 
                  } 
                 
                  public CountDownLatch getLatch() { 
                    return latch; 
                  } 
                 
                } 

Puntos clave

  • Los MOM son muy útiles para comunicar los microservicios o aplicaciones monolíticas.

  • No hay muchas diferencias a nivel de código entre las diferentes soluciones de MOM.

  • Tenga en cuenta que la arquitectura puede evolucionar.

  • Los MOM permiten eliminar la tensión en ciertos cuellos de botella, suavizando la carga en las llamadas.

VM6883:64

Introducción Kotlin

Kotlin es un lenguaje de programación de tipo estático, que se ejecuta en la JVM. JetBrains lo desarrolla desde 2010. Puede funcionar en un entorno nativo sin JVM, en un entorno JavaScript y en un entorno JVM. En este capítulo, solo abordaremos la versión del lenguaje que se ejecuta en una JVM. Del mismo modo, solo veremos algunos aspectos de este lenguaje porque se necesitaría un libro completo para describir íntegramente su integración con Spring. Tendremos una visión general de los nuevos conceptos aportados por al lenguaje.

JetBrains se creó en 2000 para hacer herramientas en varios lenguajes. La empresa JetBrains se distinguió por su editor IntelliJ IDEA y sus derivados (como PhpStorm para PHP) y recibió el premio Most Innovative Java Compagnie en 2012. IntelliJ IDEA

Los aspectos fundamentales de Kotlin son:

Aspecto

Aspiración

Concisión

No hay código único para todos

Expresividad

Grandes ideas en pocas palabras

Interoperabilidad

Para soportar código existente

Pragmatismo

Atención a la vida real

Kotlin toma ideas de Java, Groovy, Scala y Ceylon, simplificándolas tanto como sea posible.

Cada vez más proyectos usan Kotlin, incluidos los proyectos de Android, para los que ahora se soporta Kotlin. Android

Ya estábamos usando Java 8 y sus nuevas funcionalidades, pero deberíamos considerar la posibilidad de migrar a Kotlin. Este capítulo muestra las características de Kotlin y su relación con el lenguaje Java porque Kotlin también fue diseñado con la preocupación de no reproducir algunos aspectos olvidados o ciertas funcionalidades desafortunadas de Java. Encontramos un enfoque muy cercano al que se implementó durante la creación del lenguaje go.

Para la versión JVM, los proyectos que usan Kotlin se pueden industrializar con Maven o Gradle. Pueden usar librerías o código Java soportado. Todo el ecosistema de Java sigue estando disponible. Podemos considerar un enfoque TDD con JUnit 5 para validar el código Kotlin y estudiar sus características lingüísticas. JUnit 5

El primer ejemplo muestra un método main que llama a una función de una clase con su prueba unitaria asociada.

Programa principal (Ejemplo1.kt):

fun main(args: Array<String>){ 
                    println("Hola a todos:)") 
                } 

Aquí vemos una función que lanza el programa, como en c.

Características principales del lenguaje Kotlin

Tenemos clases, como en Java, pero estas clases son públicas por defecto. Veremos que es posible utilizar los plugins de Maven y Gradle para tener en cuenta este aspecto.

1. Los métodos y las funciones

He aquí un ejemplo de una función:

fun maxOf(a: Float, b: Float) = if (a > b) a else b 

Vemos que:

  • el tipo tiene el sufijo,

  • hay una sintaxis corta para las funciones que caben en una línea,

  • el if devuelve un valor y, por lo tanto, se puede utilizar en una expresión,

  • todo está tipado estáticamente.

2. La inmutabilidad de los objetos

En Java, algunos objetos son inmutables, como las constantes y algunas colecciones, pero, en general, es muy posible modificar el contenido de un objeto pasado como argumento de un método y este intercambio de estados se vuelve muy complejo de gestionar. Una buena práctica es no modificar nunca un objeto pasado como argumento, sino devolver una copia modificada como con la clase String en Java. Algunos proyectos en Java usan la librería Immutable: (https://immutables.github.io/), pero esto sigue siendo un truco en Java y la inmutabilidad se puede evitar con reflexibilidad y las listas son vulnerables.

Los inmutables son thread-safe y resultan ideales para las claves de Map y de Set, se pueden almacenar en caché y la validación de los campos se realiza durante la creación del objeto (o su copia extendida).

El proyecto Lombok ya ofrece las val y var, que está cerca de lo que se hace en Kotlin. JEP 286 aborda este tema para Java: http://openjdk.java.net/jeps/286. JEP es el eje de todas las propuestas de mejora del JDK para OpenJDK.

En Kotlin, la inmutabilidad de los objetos, que es una garantía de buen funcionamiento, se gestiona a través del tipo val:

var x // objeto mutable 
                val y // Objeto inmutable 

3. Los tipos

Hay una inferencia de tipos:

//Inferido 
                    val y1 = "abc" 
                    val y2 = 4 
                 
                //Declarado implícitamente 
                    val y3: List<String> = ArrayList() 
                    val y4: Double = 3.14159 

Hay adaptaciones inteligentes de tipo:

    val obj = 2.5 
                    if (obj is Double) { 
                        println(obj.inc()) 
                    } 

Hay una interpolación de cadena de caracteres:

    val x5 = 1 
                    val x6 = 9 
                    println("la suma de $x5 y $x6 es ${x5 + 6}") 
                //Interpolación de los String 
                    assertEquals("la suma de $x5 y $x6 es ${x5 + 6}""la suma de 1 y 9 es 10", "err interpolation") 

La interpolación se realiza mediante un motor de plantillas integrado en el lenguaje que se puede utilizar para crear páginas web dinámicas.

Hay un nuevo operador de comparación llamado «Referencia» con el triple igual (===).

Igualdad intuitiva: sea una clase Polygon con su hashcode y métodos hashcode equals:

class Polygone(val name: String) { 
                    override fun equals(other: Any?): Boolean { 
                        if (this === other) return true 
                        if (javaClass != other?.javaClass) return false 
                        other as Polygone 
                        if (name != other.name) return false 
                        return true 
                    } 
                    override fun hashCode(): Int { 
                        return name.hashCode() 
                    } 
                } 

Hay argumentos con valores predeterminados y argumentos opcionales:

    fun buildPolygon1(nom: String, angExt: Int = 800angInt : Int = 600) { 
                    } 
                    buildPolygon1("Heptagone") 
                    buildPolygon1("Heptagone", 51) 
                    buildPolygon1("Heptagone", 51, 128) 

Esto permite pasar los var-args (lista de argumentos variables) o métodos polimórficos con multifirma de Java, acompañados por el constructor que inicializa los valores predeterminados.

Hay argumentos con nombre:

    fun buildPolygon2(nom: String, angExt: Int = 90, angInt: Int = 90) { 
                    } 
                    buildPolygon2("Cuadrado",90, 90) 
                    buildPolygon2(nombre = "Pentágono", angExt = 108) 
                    buildPolygon2(angInt = 36, nombre = "Decágono") 

Esto permite tener un orden en los argumentos, diferente al orden seguido en la firma del método o función, y poder omitir un argumento opcional, que puede ser diferente al último argumento de la firma.

Existe un when «funcional»:

    val angulo=2 
                    when(angulo) { 
                        1 -> println("el ángulo es 1") 
                        2 -> println("el ángulo es 2") 
                        else -> println("el ángulo está fuera de rango") 
                    } 

Existe un sistema de propiedades que genera automáticamente descriptores de acceso manteniendo la posibilidad de sobrecargarlos.

Para la clase:

class Rectangulo () { 
                    private var longitud: Int = 0 
                    var larg : Int = 0 
                 
                    var long 
                        get() = this.longitud 
                        set(value) { 
                            longitud = value 
                        } 
                    var surface: Int = 0 
                        get() = this.longitud *this.larg 
                }  

Podemos tener:

    val rect1 = Rectangulo() 
                    //rect1.longitud=5 : err compilation 
                    rect1.long=4 
                    rect1.larg=5 
                    println("Rectángulo superf:${rect1.surface}") 

Hay un error de compilación porque no se puede acceder directamente a la visibilidad privada de ’longitud’.

Hay un sistema de extensión de funciones que permite añadir comportamientos adicionales a un tipo existente:

public inline fun String.monformat(): String = (this as 
                java.lang.String).replace('v','^') 

El inline significa que la función forma parte de una expresión y, por lo tanto, se puede colocar después del =, y que el return es opcional.

    val myStr = "Estoesunaprueba" 
                    val formatted = myStr.miformato() 
                    println(formatted) // Muestra: "Esto^es^una^prueba" 

Kotlin incorpora una serie de sobrecargas, como replaceAfter:

    val myStr2 = "esto es una prueba/Fase1" 
                 
                    // --> esto es una prueba/F-un 
                    println(myStr2.replaceAfter("/","Ph-un")) 
                 
                    // --> esto es una prueba/Fase1 
                    println(myStr2.capitalize()) 
                 
                    // --> Fase1 
                    println(myStr2.substringAfterLast("/")) 
                 
                    // --> usr/bin/readme 
                    val myFile = "/usr/bin/readme.txt" 
                    println(myFile.removeSuffix(".txt"))// end::junit5ex15[] 

Kotlin ofrece APIs extendidos específicos para Spring:

  • ApplicationContext

  • MVC de Spring

  • Spring WebFlux

  • RestTemplate

  • JDBC

4. Gestión de valores nulos

En Java, es difícil manejar el puntero null porque a menudo provoca una excepción NullPointerException en una ubicación inesperada. En Java, se pueden usar las anotaciones formales semánticas IntelliJ: @Nullable y @NotNull. Cuando se utiliza Hibernate Validator o Bean Validation, también hay anotaciones similares. El uso de Optional es un intento de resolver valores nulos en Java. IntelliJ

Kotlin proporciona un manejo más intuitivo para valores nulos.

    var s1: String = "un valor" 
                    //s1 = null // Error durante la compilación 
                 
                    var s2: String? = "otro valor" 
                    s2 = null // Sin error durante la compilación 
                 
                    //Prueba de la llamada de un método o lectura de propiedad 
                en una instancia nula: 
                    //val l1 = s2.length // Error durante la compilación: s2 puede ser null 
                    val l2 = s2?.length //El tipo de l2 es un Int nullable 
                    println(l2) // muestra null 

No es posible poner un valor nulo porque no hay ? después del tipo, lo que significaría que cero es posible.

5. Llamadas encadenadas seguras

Para las clases:

class Cap() { 
                    var nombre: String?=null 
                } 
                class Doc(var cap: Cap) { 
                } 

La variable nombre puede ser un String que sea null.

Tenemos una seguridad:

    val cap = Cap() 
                    cap.nombre ="Kotlin es genial" 
                    val doc1 = Doc(cap=cap) 
                    val nombre = doc1?.cap?.nombre ?: "desconocido" 
                    println(nombre) // muestra Kotlin es genial 
                    val cap2 = Cap() 
                    val doc2 = Doc(cap2) 
                    val nombre2 = doc2?.cap?.nombre ?: "desconocido" 
                    println(nombre2) // muestra desconocido 

6. Las lambdas

Las lambdas son extendidas. Es posible identificar una lambda por su firma.

    val multiplicacion = { a:Int, b:Int -> a * b } 
                    var resultado = multiplicacion(6,4) 
                    println(resultado) // muestra 24 
                 
                    val numeroMap = mapOf("clave1" to 1, "clave2" to 2, "clave3" to 3) 
                    val res2 = numeroMap.filterValues { it > 1 } 
                    println(res2) 
                 
                    var suma = 0 
                    numeroMap.filter { it.value > 1 }.forEach { 
                        suma += it.value 
                    } 
                    println(suma) 
                 
                    var suma2 = numeroMap.filter { it.value > 1 }.map { it.value }.sum() 
                    println(suma2) 

Controlador Spring MVC, Spring Boot en Kotlin

Utilizar Spring Initializr en https://start.spring.io/ para crear una aplicación Maven Kotlin:

Ítem

valor

Group

fr.eni.kotlin.spring5.mvc

Artefact

kotlin

Name

kotlin

Description

Ejemplo Spring 5 Kotlin Spring Boot Spring MVC

Package Name

fr.eni.kotlin.spring5.mvc.app

Packaging

jar

Java Version

17

Spring Boot

2.6.7

Tabla 3. Argumentos.

Nombre de las dependencias

Spring Web

Mustache

Spring Data JPA

H2 Database

Spring Boot DevTools

Rest Repository HAL Browser

Tabla 4. Dependencias.

Tenemos las dependencias:

Librería

Utilidad

kotlin-stdlib-jdk8

Variante Java 8 de la librería estándar Kotlin.

kotlin-reflect

La librería de reflexión de Kotlin.

jackson-module-kotlin

Soporte para serialización/deserialización de clases.

1. Función principal

Tenemos la clase para lanzar nuestro programa:

KotlinAplicación:

@SpringBootApplication 
                class KotlinApplication 
                 
                fun main(args: Array<String>) { 
                       runApplication<KotlinApplication>(*args) 
                } 

2. Prueba asociada a la función principal

He aquí hay una prueba de la clase del programa principal.

KotlinApplicationTests.kt:

@SpringBootTest 
                class KotlinApplicationTests { 
                   @Test 
                   fun contextLoads() { 
                   } 
                } 

Debe configurar la base de datos H2 en el archivo application.properties:

spring.datasource.url = jdbc:h2:~/test 
                spring.datasource.username = sa 
                spring.datasource.password = 
                spring.jpa.properties.hibernate.dialect = 
                org.hibernate.dialect.H2Dialect 
                spring.jpa.hibernate.ddl-auto = update 

Pasar el scope de h2 de runtime a compile en el pom.xml.

Añadir un perfil de ejecución:

fr-eni-kotlin-spring5-mvc/src/main/kotlin/fr/eni/kotlin/spring5/mvc/app/config/ProfileExecution.kt

object ProfileExecution { 
                    const val DEV = "dev" 
                    const val TEST = "test" 
                    const val PROD = "prod" 
                } 

Añadir la variable de entorno: SPRING_PROFILES_ACTIVE=test.

Configurar H2 para tener una consola accesible desde la aplicación, con el perfil test:

fr-eni-kotlin-spring5-mvc/src/main/kotlin/fr/eni/kotlin/spring5/mvc/app/config/ProfileExecution.kt

@Configuration 
                @Profile(ProfileExecution.TEST) 
                @EnableJpaRepositories("fr.eni.kotlin.spring5.mvc.app.repositories") 
                open class InMemoryDataSource { 
                 
                    /** 
                     * Registro del servlet de la consola H2 
                     * localhost:8080/console 
                     */ 
                    @Bean 
                    fun h2servletRegistration(): ServletRegistrationBean<*> { 
                        val registration = ServletRegistrationBean(WebServlet()) 
                        registration.addUrlMappings("/console/*") 
                        return registration 
                    } 
                 
                    @Bean(initMethod = "start", destroyMethod = "stop") 
                    open fun h2TCPServer(): Server? { 
                        return Server.createTcpServer("-tcp", "-tcpAllowOthers") 
                    } 
                } 

Crear la clase de dominio:

src/main/kotlin/fr/eni/kotlin/spring5/mvc/app/domain/Cuenta.kt

@Entity 
                data class Cuenta( 
                        @Id 
                        @GeneratedValue(generator = "uuid") 
                        @GenericGenerator(name = "uuid", strategy = "uuid2") 
                        @Type(type = "uuid-char") 
                        var id: UUID? = null, 
                        var aperturaDateTime: LocalDateTime = LocalDateTime.now(), 
                        var nombre: String = "" 
                ) 

Crear el Repository:

CuentaRepository.kt

@Repository 
                interface CuentaRepository : JpaRepository<Cuenta, UUID> 

Añadir una versión de la API en la URL:

application.properties

api.version=1 
                api.base=/api/v${api.version}/ 

/src/main/resources/application.properties

Ahora creamos el controlador que expone los recursos como servicios REST:

CuentaController.kt

@RestController 
                @RequestMapping(value = "\${api.base}", produces = 
                arrayOf(MediaType.APPLICATION_JSON_UTF8_VALUE)) 
                class CuentaController { 
                    @Autowired 
                    lateinit private var CuentaRepository: CuentaRepository; 
                    @RequestMapping("/Cuentas", method = arrayOf(RequestMethod.GET)) 
                    fun getCuentaPosts() = CuentaRepository.findAll() 
                 
                    @RequestMapping("/Cuenta/{id}", method = 
                arrayOf(RequestMethod.GET)) 
                    fun getCuentaPost(@PathVariable id: UUID) = 
                CuentaRepository.findById(id); 
                 
                    @RequestMapping("/Cuenta/{id}", method = 
                arrayOf(RequestMethod.PATCH)) 
                    fun updateCuentaPost(@PathVariable id: UUID, @RequestBody nombre: 
                String) { 
                        var Cuenta : Optional<Cuenta> = 
                CuentaRepository.findById(id); 
                        Cuenta.get().nombre = nombre 
                        CuentaRepository.save(Cuenta.get()); 
                    } 
                 
                    @RequestMapping("/Cuenta/{id}", method = 
                arrayOf(RequestMethod.DELETE)) 
                    fun deleteCuentaPost(@PathVariable id: UUID) = 
                CuentaRepository.deleteById(id); 
                 
                    @RequestMapping(value = "/Cuenta", method = 
                arrayOf(RequestMethod.POST)) 
                    fun postCuentaPost(@RequestBody nombre: String) = 
                CuentaRepository.save(Cuenta(nombre = nombre)) 
                } 

Ahora tenemos un back operativo. Podemos usar el HAL Browser para rellenar datos en la URL http://localhost:8080/

VM6883:64

Los plugins

Los plugins Maven para la versión Spring de Java también están disponibles para Spring Kotlin. El plugin all-open se creó porque Spring necesita sobrecargar los métodos cglib para sus proxies. Kotlin declara sus clases y métodos finales de forma predeterminada, lo que no permite que Spring se sobrecargue. Por lo tanto, es necesario declarar sistemáticamente las API de los @Bean publicados a través del operador open o usar el plugin Kotlin Spring, que lo hace por usted.

Es posible usar el plugin all-open Maven o Gradle para hacer que las clases y los métodos sean automáticamente open (públicos).

En el código siguiente, sin el plugin Maven, necesitamos establecer open para especificar que la clase y el método son públicos, porque son privados por defecto con Kotlin, a diferencia de lo que ocurre con Java.

@SprinBootApplication 
                open class Application { 
                  @Bean 
                  open fun fonction1() = ... 
                } 

con:

@SprinBootApplication 
                 
                class Application { 
                  @Bean 
                  fun fonction1() = ... 
                } 

Build Maven

<build> 
                  <sourceDirectory>${project.basedir}/src/main/kotlin</ 
                sourceDirectory> 
                  <testSourceDirectory>${project.basedir}/src/test/kotlin</ 
                testSourceDirectory> 
                  <plugins> 
                   <plugin> 
                    <groupId>org.springframework.boot</groupId> 
                    <artifactId>spring-boot-maven-plugin</artifactId> 
                   </plugin> 
                   <plugin> 
                    <groupId>org.jetbrains.kotlin</groupId> 
                    <artifactId>kotlin-maven-plugin</artifactId> 
                    <configuration> 
                     <args> 
                      <arg>-Xjsr305=strict</arg> 
                     </args> 
                     <compilerPlugins> 
                      <plugin>spring</plugin> 
                      <plugin>jpa</plugin> 
                     </compilerPlugins> 
                    </configuration> 
                    <dependencies> 
                     <dependency> 
                      <groupId>org.jetbrains.kotlin</groupId> 
                      <artifactId>kotlin-maven-allopen</artifactId> 
                      <version>${kotlin.version}</version> 
                     </dependency> 
                     <dependency> 
                      <groupId>org.jetbrains.kotlin</groupId> 
                      <artifactId>kotlin-maven-noarg</artifactId> 
                      <version>${kotlin.version}</version> 
                     </dependency> 
                    </dependencies> 
                   </plugin> 
                  </plugins> 
                 </build> 
VM6883:64

Puntos clave

  • Kotlin podría sustituir a Java y usar la JVM.

  • Spring ha trabajado mucho en la integración de Kotlin.

  • JetBrains, que hizo el excelente IntelliJ IDEA, ha creado un lenguaje muy potente. 

  • Es posible convertir Java a Kotlin copiando/pegando en IntelliJ IDEA. IntelliJ

VM6883:64

Introducción Reactor WebFlux

Este capítulo presenta brevemente las novedades responsivas de Spring 5.

El objetivo de la programación responsiva es simplificar la escritura de aplicaciones asíncronas que, en lugar de utilizar el mecanismo clásico de un pool voluminoso de threads bloqueantes, utiliza un bucle y un número mínimo de threads gracias a un motor basado en un framework NIO como Netty. Netty

Ponemos en cola los procesamientos rápidos de realizar, y se llevan a cabo a lo largo del tiempo. Entonces, es posible modelar estos procesamientos en forma de flujos y propagaciones de cambios. Podemos gestionar flujos estáticos con tablas o flujos dinámicos con transmisores y receptores de eventos, como en el design pattern observador.

En un contexto de servidor de un clúster elástico, este tipo de procesamiento «en serie» permite saber si la carga en un servidor está alineada con la potencia del clúster a través de la backpressure, que mide el volumen del flujo aceptable en la entrada del servidor. Si se supera un umbral, podemos aumentar el número activo de nodos en el clúster para aligerar la carga por servidor y, seguidamente, reducir el número de nodos si la carga baja.

El objetivo es que los productores (Publisher) no abrumen a los consumidores (Consumer). Esto es particularmente adecuado para microservicios en el cloud. Publisher Consumer

El diseño de servidores responsivos es más complejo que el de los servidores tradicionales. De hecho, entramos en el mundo de lo asíncrono, que choca con el modo de programación funcional (basado en lambdas).

El uso de bucles de eventos permite lanzar muchos menos threads y aprovechar al máximo los núcleos de los procesadores de la máquina. En un entorno cloud, los procesadores también se utilizan mejor.

Spring Reactor Reactor

1. Presentación

Históricamente, teníamos RxJava 1 y 2 para hacer programación responsiva.

Spring decidió implementar Reactor, su propia versión de 4.ª generación de un motor responsivo, para hacer el mejor uso de las tecnologías actuales.

El reactor está particularmente bien adaptado a Spring. Puede interactuar fácilmente con RxJava y con el equivalente del JDK9: java.util.concurrent.Flow. Flow

Una aplicación responsiva se caracteriza por estos puntos:

  • Responsivo: proporciona tiempos de respuesta rápidos y consistentes.

  • Resiliente: sigue siendo responsivo en caso de error y se debe recuperar.

  • Elástico: sigue siendo responsivo y es capaz de manejar varias cargas de trabajo.

  • Message driven: se comunica mediante mensajes asíncronos.

Las aplicaciones responsivas no son más rápidas que las tradicionales, sino que tienen un comportamiento mucho mejor controlado y predecible cuando el servidor llega a saturarse. Aunque la aplicación Java esté saturada, el sistema host sigue siendo totalmente accesible. Las aplicaciones son más intuitivas de programar y mejor estructuradas a través de la programación funcional.

Por un lado, tenemos el Publisher (editor) que produce uno o más elementos, que posteriormente los consume un Consumer (consumidor). Se debe considerar que estos elementos intercambiados son eventos.

Cabe destacar que la programación es asíncrona. El código ya no se ejecuta secuencialmente. Dos procesos de flujo que se suceden en el código se inician en paralelo. No esperamos a que una secuencia termine de procesarse para pasar al siguiente flujo. Paradójicamente, el sistema solo inicia un proceso a la vez en su cola. Por lo tanto, será necesario sincronizar los flujos.

Hay dos tipos de Publisher:

  • El que emite 0 o 1 elemento:

    Mono: reactor.core.publisher.Mono<T> Mono

  • El que emite de 0 a N elementos:

    Flux: reactor.core.publisher.Flux<T> Flux

Es posible utilizar Mono y Flux sin utilizar programación funcional. Sin embargo, las lambdas son más concisas e integradas en java 8. Por lo tanto, nos centramos solo en ejemplos que usan la programación funcional.

La programación funcional implica conocer bien las lambdas.

La programación de un Publisher se realiza en tres pasos:

  • Crear el flujo.

  • Rellenar de contenido el flujo.

  • Consumir el flujo a través de una suscripción a este.

Considere el siguiente ejemplo:

Flux.just("a", "b", "c", "d", "e", "f", "g", "h") 
                .log() 
                .subscribe(); 

No pasa nada si no se suscribe al flujo.

Es posible realizar acciones específicas en el subscribe, pasando un objeto que implemente la interfaz Subscriber. Esta interfaz tiene cuatro métodos básicos, que se corresponden con las cuatro fases del ciclo de vida de un elemento:

Método

Uso

onSubscribe onSubscribe

Se llama después de haber llamado a Publisher.subscribe(Subscriber).

onNext onNext

Notificación de datos enviada por el editor en respuesta a las solicitudes a Subscription.request(long).

onError onError

Estado del terminal con errores.

onComplete onComplete

Estado del terminal correcto.

class MiSubscriber implements Subscriber<String> private Logger logger = 
                LoggerFactory.getLogger(MiSubscriber.class); 
                @Override 
                public void onSubscribe(Subscription subscription) { 
                 logger.info("onSubscribe() -> {}", Long.MAX_VALUE); 
                 subscription.request(Long.MAX_VALUE); 
                } 
                @Override 
                 public void onNext(String item) { 
                 logger.info("onNext() -> {}", item); 
                } 
                @Override 
                 public void onError(Throwable throwable) { 
                 logger.error("onError()", throwable); 
                } 
                @Override 
                 public void onComplete() { 
                 logger.info("onComplete -> Terminado."); 
                 } 
                } 

A menudo cometeremos el error de no manejar casos de errores. Es posible encadenar flujos y la suscripción se realiza en el flujo al final de la cadena.

Flujo 1-> Flujo B-> Flujo C

En el caso más complejo, es posible dividir un flujo en varios y agrupar un conjunto de ellos en uno solo utilizando filtros a través de operadores de flujo y sincronización. En términos generales, podemos hacer de una manera más sencilla todo lo que hacíamos antes en programación asíncrona con threads.

Podemos usar Reactor para implementar un servicio.

En la entrada de un servicio, generalmente tenemos:

  • una consulta HTTP para un servidor REST,

  • una consulta WebSocket,

  • una cola de mensajes si tiene un middleware orientado a mensajes (message-oriented middleware o MOM),

  • pero también un e-mail, un archivo si hace CFT, etc.

Nos ocupamos de estos elementos uno por uno, realizando una serie de pasos.

Ya podemos asumir que cada paso del procesamiento debe ser responsivo y que no debe haber elementos bloqueantes dentro del procesamiento.

Si se deben esperar dos elementos, se debe utilizar un operador de sincronización porque un proceso nunca se debe bloquear. Si, a pesar de todo, esto sucede, se debe gestionar de manera específica. Esto se denomina aplicación híbrida. El procesamiento de bloqueo se maneja en un bucle de eventos, separado con threads de bloqueo.

El motor responsivo para Spring es Reactor y hay una supercapa correspondiente a Spring MVC, pero responsiva, que es WebFlux.

2. Uso de Reactor Core

Usamos Reactor para hacer lo que normalmente haríamos con la programación asíncrona usando threads.

Un programa Java asíncrono tradicional utiliza threads, FutureTask y CompletableFuture. Usamos programación asíncrona para paralelizar tareas largas, para que podamos continuar una tarea principal mientras esperamos que se completen las tareas secundarias. FutureTask CompletableFuture

a. Los threads

Podemos crear un nuevo thread para realizar una operación de forma asíncrona.

Vamos a crear un nuevo thread que compruebe si un número es primo y muestre el resultado:

int numero = 1234567; 
                Thread nuevoThread = new Thread(() -> { 
                   System.out.println(" number + " es un número primo "+  
                esPrimo (numero)); 
                }); 
                nuevoThread.start(); 

b. Las FutureTask

A partir de Java 5, la interfaz Future proporciona una forma de realizar operaciones asíncronas utilizando FutureTask. Usamos el método submit de ExecutorService para realizar la tarea de forma asíncrona y devolver la instancia de FutureTask.

ExecutorService threadpool = Executors.newCachedThreadPool(); 
                Future<Boolean> futureTask = threadpool.submit(() -> 
                estPrimo(number)); 
                 
                while (!futureTask.isDone()) { 
                   System.out.println("FutureTask está en curso..."); 
                } 
                Boolean resultado = futureTask.get(); 
                 
                threadpool.shutdown(); 

Utilizamos el método isDone proporcionado por la interfaz Future para comprobar si la tarea se ha completado. Una vez hecho esto, podemos recuperar el resultado usando el método get.

c. CompletableFuture

Java 8 introdujo CompletableFuture con una combinación de Future y de CompletionStage.

Proporciona varios métodos para la programación asíncrona:

supplyAsync

Devuelve un nuevo CompletableFuture terminado de forma asíncrona por una tarea, con el valor obtenido al llamar al Supplier suministrado.

runAsync

Devuelve un nuevo CompletableFuture terminado de forma asíncrona por una tarea en ejecución después de realizar la acción especificada.

thenApplyAsync

Devuelve un nuevo CompletionStage que, cuando este paso termina con normalidad, se ejecuta con el resultado de este paso como argumento de la función proporcionada.

Por ejemplo:

CompletableFuture<Long> completableFuture =  
                CompletableFuture.supplyAsync(() -> esPrimo(number)); 
                while (!completableFuture.isDone()) { 
                   System.out.println("CompletableFuture está en curso..."); 
                } 
                Boolean resultado = completableFuture.get(); 

No necesitamos usar ExecutorService de forma explícita. CompletableFuture utiliza internamente un ForkJoinPool para manejar la tarea de forma asíncrona.

También podemos utilizar la anotación @Async, que permite lanzar una tarea en un segundo thread.

Estos métodos tienen el efecto de lanzar una multitud de threads.

d. Flux y Mono

Utilizamos la programación responsiva para paralelizar tareas cortas, para poder hacer el mejor uso posible de la potencia de una máquina.

Por ejemplo, usamos dependencias Maven:

<dependency> 
                   <groupId>io.projectreactor</groupId> 
                   <artifactId>reactor-core</artifactId> 
                   <version>3.3.10.RELEASE</version> 
                </dependency> 
                <dependency> 
                   <groupId>ch.qos.logback</groupId> 
                   <artifactId>logback-classic</artifactId> 
                   <version>1.2.3</version> 
                </dependency> 

Aunque Reactor ha colocado Mono y Flux en el paquete reactor.core.publisher, tenemos suscriptores y editores de eventos.

Un método que devuelve un Mono o un Flux es un editor (publisher).

El código que consume un Mono o un Flux es un suscriptor.

Por lo tanto, el flujo se declara en el editor y se ejecuta en el suscriptor.

Por ejemplo, en el código del R2DC encontramos:

@Override  
                   public Mono<Void> releaseSavepoint(String name) { 
                       Assert.requireNonNull(name, "name must not be null"); 
                 
                       return useTransactionStatus(inTransaction -> { 
                           if (inTransaction) { 
                               this.client.execute(String.format("RELEASE  
                SAVEPOINT %s", name)); 
                           } else { 
                               this.logger.debug("Skipping release savepoint  
                because no transaction in progress."); 
                           } 
                 
                           return Mono.empty(); 
                       }) 
                           .onErrorMap(DbException.class,  
                H2DatabaseExceptionFactory::convert); 
                   } 

Entonces, el objetivo es configurar una secuencia de «funciones» que se ejecutan puntualmente o cada vez que se recibe un evento.

Las secuencias se realizan de suscriptores a suscriptores, con posibles transformaciones del tipo de mensaje. Entonces, es posible definir circuitos en una red mallada para procesar flujos de mensajes.

Por lo tanto, la creación del encadenamiento generalmente no se realiza en el mismo lugar que el consumo del flujo, excepto en ciertos casos en los que desea mezclar código de procedimiento estándar con código responsivo a través del método just(flux).

He aquí un primer ejemplo de flujo múltiple (Flux) y un segundo ejemplo de flujo unitario (Mono):

Flux<Float> just = Flux.just(1.0, 3.0, 5.0, 7.0, 11.0); 
                Mono<Float> just = Mono.just(1.0); 

Es necesario distinguir entre Mono y Flux porque no tienen exactamente la misma API. Esto también permite la interoperabilidad con RxJava, que tiene flujos similares.

Operadores de recogida

Después de la creación de un flujo, aquí estático, podemos consumirlo para realizar procesamientos.

A continuación, se muestra un ejemplo que ilustra el principio:

List<String> cadenas = new ArrayList<>(); 
                Flux.just("aaa", "bbb", "ccc") 
                 .log() 
                 .subscribe(cadenas::add); 
                 
                assertThat(cadenas).containsExactly("aaa", "bbb", "ccc"); 

Normalmente, un flujo no modifica elementos fuera del flujo porque esto viola el principio de inmutabilidad (inmutabilidad de los datos). Aquí modificamos el contenido de la lista como parte de nuestros experimentos, pero no lo haríamos en un código de negocio.

El método log() del ejemplo anterior se utiliza para registrar las operaciones:

onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription) 
                request(unbounded) 
                onNext("aaa") 
                onNext("bbb") 
                onNext("ccc") 
                onComplete()* 

Esto se parece a la versión clásica con Java 8 Streams:

List<String> collected = Stream.of("aaa", "bbb", "ccc")  
                 .collect(toList()); 

La diferencia está en los elementos que se transmiten a los suscriptores y se procesan a medida que llegan. Las operaciones de paralelización de los Streams tienen un propósito diferente a las de las API responsivas.

Los Streams permiten separar un procesamiento pesado y distribuirlo en varios procesadores para luego agrupar los datos, mientras que la API responsiva está destinada a encadenar un máximo de operaciones cortas en un bucle de eventos.

Los Flux también permiten disponer de flujos de eventos infinitos a través de la escucha de un socket de red no bloqueante, por ejemplo.

Los tiempos de ejecución son importantes

Podemos realizar experimentos añadiendo retrasos para un Mono y un Flux:

  • delaySubscription(Duration delay) retrasa la suscripción a esta fuente Mono hasta que haya transcurrido el período especificado.

  • delayElements(Duration delay) retrasa cada elemento del Flux (señales Subscriber.onNext (T)) en una duración determinada en milisegundos.

Para tareas largas es posible utilizar Schedulers.parallel(): Schedulers.parallel

Flux.just("a", "b", "c", "d") 
                 .log() 
                 .map(i -> i + "z") 
                 .subscribeOn(Schedulers.parallel()) 
                 .subscribe(elements::add); 

El programador paralelo hace que nuestra suscripción se ejecute en un thread diferente. El flujo se ejecuta en otro thread llamado parallel-1.

Es posible combinar flujos. Hay multitud de operadores:

@Test 
                public void firstEmitting() { 
                 Mono<String> a = Mono.just("accción A") 
                                     .delaySubscription(Duration.ofMillis(550)); 
                 Flux<String> b = Flux.just("Levantarse", "Comer",  
                "Echarse la siesta") 
                                     .delayElements(Duration.ofMillis(450); 
                 
                 Flux.first(a, b) 
                     .toIterable() 
                     .forEach(System.out::println); 
                } 

Cada parte de la oración incluye una breve pausa de 450 ms. Los Flux también se utilizan para gestionar la contrapresión (backpressure).

Podemos transmitir al editor un número de elementos por paquete para enviarlos a través del método request() de la clase Subscription:

Flux.just("a", "b", "c", "d", "e", "f") 
                 .log() 
                 .subscribe(new Subscriber<String>() { 
                   private Subscription s; 
                   int onNext; 
                 
                   // Pide 3 a la suscripción 
                   @Override 
                   public void onSubscribe(Subscription s) { 
                       this.s = s; 
                       s.request(3); 
                   } 
                 
                   @Override 
                   public void onNext(String st) { 
                       elements.add(st); 
                       onNext++; 
                       //Vuelve a pedir 3 más 
                       if (onNext % 3 == 0) { 
                           s.request(3); 
                       } 
                   } 
                 
                   @Override 
                   public void onError(Throwable t) {} 
                 
                   @Override 
                   public void onComplete() {} 
                }); 

Esta es una contrapresión responsiva de tracción.

Pedimos empujar hacia adelante un determinado número de elementos, solo cuando estemos listos.

3. Las pruebas

Las pruebas de métodos responsivos son complejas porque funcionan de forma asíncrona. Este modo implica nociones de tiempos de ejecución y retrasos en las pruebas, que requieren el uso de programadores y relojes virtuales.

a. Comprobaciones con StepVerifier StepVerifier

Usamos la clase StepVerifier, que se encuentra en el módulo complementario reactor-prueba.

Un StepVerifier proporciona una forma declarativa de crear un script verificable para una secuencia asíncrona de Publisher, expresando expectativas relativas a los eventos que se producirán durante la suscripción.

La comprobación se debe activar después de que se hayan declarado las expectativas del terminal (finalización, error, cancelación, etc.), llamando a uno de los métodos verify().

Es necesario:

  • crear un StepVerifier alrededor de un editor, usando create(Publisher) o withVirtualTime(Supplier<Publisher>),

  • configurar las expectativas de valor individuales mediante expectNext, expectNextMatches(Predicate), assertNext(Consumer), expectNextCount(long) o expectNextSequence(Iterable),

  • desencadenar acciones de suscripción durante la verificación mediante thenRequest(long) o thenCancel(),

  • finalizar el escenario de prueba usando una pausa de terminal: expectComplete(), expectError(), expectError(Class), expectErrorMatches(Predicate) o thenCancel(),

  • desencadenar la verificación del StepVerifier obtenido en su Publisher, usando verify() o verify(Duration).

Si las expectativas fallan, se genera un AssertionError que indica los fallos. 

Aquí hay un ejemplo de uso:

StepVerifier.create(Flux.just("foo", "bar")) 
                  .expectNext("foo") 
                  .expectNext("bar") 
                  .expectComplete() 
                  .verify(); 

También podemos usar el tiempo virtual con el generador StepVerifier.withVirtualTime, que toma un Proveedor<Editor>.

Veamos algunos ejemplos simples que muestran cómo funciona StepVerifier.

Necesitamos agregar las siguientes dependencias de prueba a nuestro POM:

<dependency> 
                 <groupId>io.projectreactor.addons</groupId> 
                 <artifactId>reactor-test</artifactId> 
                 <version>3.0.7.RELEASE</version> 
                 <scope>test</scope> 
                </dependency> 
                 
                <dependency> 
                 <groupId>org.assertj</groupId> 
                 <artifactId>assertj-core</artifactId> 
                 <version>23.22.0</version> 
                 <scope>test</scope> 
                </dependency> 

Para una clase responsiva llamada MiLibreriaResponsiva que produce algunos flujos que queremos probar:

@Component 
                public class MiLibreriaResponsiva { 
                 
                 public Flux<String> numero10(int from) { 
                    return Flux.range((int) from, 10) 
                         .map(i -> i +""+  i); 
                 } 
                 
                 public Mono<String> conPausa(String valor, int delaySeconds) { 
                    return Mono.just(valor) 
                               .delaySubscription(Duration.ofSeconds(delaySeconds)); 
                 } 
                } 

El primer método está destinado a devolver los siguientes 10 números (incluyendo el número inicial dado). El segundo método devuelve un flujo que emite un valor dado, pasado un tiempo determinado en segundos.

Queremos una primera prueba que verifique que la llamada de numero10 desde el número 20 limita su salida a 20, 21 y 22:

@Test 
                public void test() { 
                 MiLibreriaResponsiva libreria = new MiLibreriaResponsiva(); 
                 StepVerifier.create(libraria.numero10(20) 
                       .expectNext(""33", "44", "55") 
                       .expectComplete() 
                       .verify(); 
                } 

Si queremos probar el método de pausa, pero sin esperar realmente el número dado de segundos, usamos el generador withVirtualTime:

@Test 
                public void testWithDelay() { 
                 MiLibreriaResponsiva libraria = new MiLibreriaResponsiva(); 
                 Duration testDuration = 
                    StepVerifier.withVirtualTime(() -> libraria.conDelay(""20, 30)) 
                                .expectSubscription() 
                                .thenAwait(Duration.ofSeconds(10)) 
                                .expectNoEvent(Duration.ofSeconds(10)) 
                                .thenAwait(Duration.ofSeconds(10)) 
                                .expectNext(""20) 
                                .expectComplete() 
                                .verify(); 
                 System.out.println(testDuration.toMillis() + "ms"); 
                } 

StepVerifier viene con muchos otros métodos para probar los Mono y los Flux.

b. Emisiones manuales con TestPublisher

Para la emisión manual, podemos usar TestPublisher:

public abstract class TestPublisher<Textends Object 
                implements Publisher<T>, PublisherProbe<T> 

Se trata de un Publisher que podemos controlar directamente, desencadenando eventos onNext, onComplete y onError con fines de prueba.

Podemos controlar su estado usando sus métodos assertXXX, generalmente en el callback de un StepVerifier.

TestPublisher es flexible en comparación con los estándares de Responsive Stream, pero poco seguro cuando se prueban varios threads en paralelo.

Este describe varios métodos:

next(T value) o next(T value, T rest)

Envía una o más señales a los suscriptores.

emit(T value)

Como next(T) pero después invoca a complete().

complete()

Finaliza un flujo con la señal de completado.

error(Throwable tr)

Finaliza un flujo con un error.

flux()

Método práctico para mapear un TestPublisher a un flujo.

mono()

Método práctico para mapear un TestPublisher en un Mono.

He aquí un ejemplo para crear un flujo:

TestPublisher .<String>create() 
                 .next("aaa", "bbb", "ccc") 
                 .error(new RuntimeException("Mi mensaje")); 

Para una clase que comprueba los MD5 de Apache Commons:

@Data 
                public class HashChecker { 
                   private final Flux<String> source; 
                   String hash = "22D22A24E777A441B2258DD8FF7F3321"; 
                   String password = "Reactor"; 
                 
                   static String getMd5Hex(String val){ 
                       return DigestUtils.md5Hex(val).toUpperCase(); 
                   } 
                 
                   public HashChecker(Flux<String> source) { 
                       this.source = source; 
                   } 
                 
                   public Flux<String> getMd5Hex() { 
                       return source.map(HashChecker::getMd5Hex); 
                   } 
                } 

Podemos crear una prueba:

final TestPublisher<String> testPublisher = TestPublisher.create(); 
                 
                HashChecker hashChecker = new HashChecker(testPublisher.flux()); 
                 
                StepVerifier.create(hashChecker.getMd5Hex()) 
                 .then(() -> testPublisher.emit("password", "Reactor", "invalide")) 
                 .expectNext("5F4DCC3B5AA765D61D8327DEB882CF99",  
                "1436185F7342E0210BE86E7E17963250", "E220F0FFF51A14C886A8836124B795CB") 
                 .verifyComplete(); 

WebFlux WebFlux

Normalmente usaremos Reactor como parte de las aplicaciones WebFlux.

De hecho, las API Responsive en flujos de eventos estáticos a menudo se sustituyen por Streams tradicionales. WebFlux es el equivalente responsivo de Spring MVC y utiliza el motor Reactor internamente.

Diseñar una aplicación WebFlux desde cero es complejo. Recientemente, JHipster permite generar aplicaciones completas usándolo, y su uso puede ahorrar mucho tiempo.

Para utilizar Spring WebFlux, debe añadir la dependencia Maven:

<dependency> 
                   <groupId>org.springframework.boot</groupId> 
                   <artifactId>spring-boot-starter-webflux</artifactId> 
                   <version>2.6.7</version> 
                </dependency> 

Hay disponible un ejemplo completo generado con JHipster en los ejemplos descargables del libro. Los detalles al respecto se estudian en un capítulo específico.

1. Definición del término responsivo

Las nociones de responsividad en WebFlux son de varios tipos. En lugar de asignar o asociar a través de un pool un thread a una consulta para gestionar todo el procesamiento de una consulta hasta que se responde, dividimos el procesamiento en pequeños elementos que se ponen en cola y se ejecutan lo antes posible.

Procesamos este elemento en un entorno asíncrono sin bloqueo.

En un servidor Spring MVC, las consultas se procesan siempre que haya threads disponibles. Cuando estos se agotan, el cliente HTTP se pone a la espera. El cliente está esperando una conexión disponible.

En un servidor Spring WebFlux, los clientes no se bloquean. Si hay demasiadas llamadas, también saturamos el sistema, pero en otro lugar, en el bucle de eventos.

La diferencia radica principalmente en el hecho de que no tenemos threads bloqueados esperando el resultado de una etapa algo larga.

Podríamos pensar que tenemos el equivalente usando Spring MVC de manera asíncrona, pero Spring Async soporta las especificaciones Servlet 3.0, mientras que Spring WebFlux soporta Servlet 3.1+. Se puede bloquear el modelo de E/S Spring Async al comunicarse con el cliente, lo que provoca un problema de rendimiento con clientes lentos, mientras que WebFlux proporciona un modelo de E/S sin bloqueo.

La lectura del cuerpo de la consulta o de partes de la consulta se bloquea en Spring Async, mientras que no es bloqueante en Spring WebFlux.

En Spring Async, los filtros y servlets funcionan de forma síncrona, pero Spring WebFlux admite la comunicación asíncrona completa.

Además, Spring WebFlux admite contrapresión responsiva.

Spring WebFlux también ofrece la posibilidad de programar en un estilo funcional gracias a la API Reactor utilizada.

2. Las capas responsivas

Un servidor Spring WebFlux completo utiliza un modelo en capas, como Spring MVC. Cada uno de ellos debe ser responsivo para que el conjunto funcione:

  • Una capa de controlador responsiva.

  • Una capa de servicio responsiva.

  • Una capa repository responsiva.

  • Una base de datos responsiva.

Las pruebas unitarias y de integración también deben incorporar el criterio responsivo.

Asimismo, es posible posicionar un servidor intermedio como un microservicio, que es, como entrada, un servidor web service y, como salida, un cliente web service que llama a otro web service. En este caso, es preferible tener una cadena de servidores responsivos (o asíncronos en algunos casos).

Pueden existir partes no responsivas, pero tendrán que ser aisladas y tratadas específicamente con Schedulers, por ejemplo.

A nivel de programación, Spring WebFlux y Spring MVC son muy similares, excepto que los objetos intercambiados entre capas son los Mono y los Flux.

Estudiaremos las capas en un enfoque top down, partiendo del controlador hacia el acceso a los datos y siguiendo el recorrido de una consulta.

a. La capa del controlador

La capa del controlador puede evitar algunas veces la capa de servicio y llamar a la capa repository directamente para las API SCURD.

Encontramos las anotaciones en la clase:

@RestController 
                @RequestMapping("/api"public class UsuarioRecurso{ 

El repository inyectado a través del constructor funciona como en Spring MVC:

public UsuarioRecurso(UsuarioRepository 
                usuarioRepository) { 
                   this.usuarioRepository = usuarioRepository; 
                } 

Crear un usuario

  @PostMapping("/usuarios") 
                 public Mono<ResponseEntity<Usuario>>  
                createUsuario(@Valid @RequestBody Usuario usuario)  
                throws URISyntaxException { 
                   log.debug("REST request to save Usuario : {}",  
                usuario); 
                   if (usuario.getId() != null) { 
                     throw new BadRequestAlertException("A new usuario  
                cannot already have an ID", ENTITY_NAME, "idexists"); 
                   } 
                   return usuarioRepository.save(usuario).map(result -> { 
                       try { 
                         return ResponseEntity.created(new  
                URI("/api/usuarios/" + result.getId())) 
                 
                  .headers(HeaderUtil.createEntityCreationAlert(applicationName,  
                true, ENTITY_NAME, result.getId())) 
                           .body(result); 
                       } catch (URISyntaxException e) { 
                         throw new RuntimeException(e); 
                       } 
                     }); 
                 } 

El usuario se crea a través de una lambda que devuelve un Mono<ResponseEntity<Usuario>>.

Editar un usuario

  @PutMapping("/usuarios") 
                 public Mono<ResponseEntity<Usuario>>  
                updateUsuario(@Valid @RequestBody Usuario usuario) throws  
                URISyntaxException { 
                   log.debug("REST request to update Usuario : {}", usuario); 
                   if (usuario.getId() == null) { 
                     throw new BadRequestAlertException("Invalid id",  
                ENTITY_NAME, "idnull"); 
                   } 
                   return usuarioRepository.save(usuario).switchIfEmpty 
                (Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND))) 
                     .map(result -> ResponseEntity.ok() 
                       .headers(HeaderUtil.createEntityUpdateAlert(applicationName, 
                true, ENTITY_NAME, result.getId())) 
                       .body(result) 
                     ); 
                 } 

Si no se encuentra el usuario, se devuelve un error con switch-IfEmpty; de lo contrario, se devuelve el usuario modificado en un Mono<ResponseEntity<Usuario>>.

Recuperar todos los usuarios

  @GetMapping("/usuarios") 
                 public Mono<List<Usuario>> getAllUsuarios() { 
                   log.debug("REST request to get all Usuarios"); 
                   return usuarioRepository.findAll().collectList(); 
                 } 

Devolvemos una lista en un Mono.

Otra versión podría ser enviar los resultados uno a uno en un Flux:

  @GetMapping(value = "/usuarios", produces =  
                MediaType.APPLICATION_STREAM_JSON_VALUE) 
                 public Flux<Usuario> getAllUsuariosAsStream() { 
                   log.debug("REST request to get all Usuarios as a stream"); 
                   return usuarioRepository.findAll(); 
                 } 

Recuperar un usuario

  @GetMapping("/usuarios/{id}") 
                 public Mono<ResponseEntity<Usuario>>  
                getUsuario(@PathVariable String id) { 
                   log.debug("REST request to get Usuario : {}", id); 
                   Mono<Usuario> usuario =  
                usuarioRepository.findById(id); 
                   return ResponseUtil.wrapOrNotFound(usuario); 
                 } 

Devolvemos un Mono<ResponseEntity<User>>.

Aquí vemos que el findById del repositorio es responsivo.

Eliminar un usuario

  /** 
                  * {@code DELETE  /usuarios/:id} : delete the "id"  
                usuario. 
                  * 
                  * @param id the id of the usuario to delete. 
                  * @return the {@link ResponseEntity} with status {@code 204  
                (NO_CONTENT)}. 
                  */ 
                 @DeleteMapping("/usuarios/{id}") 
                 @ResponseStatus(code = HttpStatus.NO_CONTENT) 
                 public Mono<ResponseEntity<Void>>  
                deleteUsuario(@PathVariable String id) { 
                   log.debug("REST request to delete Usuario : {}", id); 
                   return usuarioRepository.deleteById(id) 
                   .then(Mono.just(ResponseEntity.noContent() 
                       .headers(HeaderUtil.createEntityDeletionAlert(applicationNametrue, ENTITY_NAMEid)).build()) 
                     ); 
                 } 
                } 

Aunque no devolvemos ningún dato al cliente, debemos devolver un Mono<ResponseEntity<Void>>.

b. La capa de los servicios

Si se desea, la capa de los servicios se puede utilizar como una conexión entre la capa del controlador y la capa repository. Se utiliza sobre todo en procesos que usan varias entidades del modelo.

Por ejemplo:

@Service 
                public class UserService { 
                 private final Logger log = 
                LoggerFactory.getLogger(UserService.class); 
                 private final UserRepository userRepository; 
                 
                 private final PasswordEncoder passwordEncoder; 
                 private final AuthorityRepository authorityRepository; 
                 
                 public UserService(UserRepository userRepository,  
                PasswordEncoder passwordEncoder, AuthorityRepository  
                authorityRepository) { 
                   this.userRepository = userRepository; 
                   this.passwordEncoder = passwordEncoder; 
                   this.authorityRepository = authorityRepository; 
                 } 
                 
                 public Mono<User> activateRegistration(String key) { 
                   log.debug("Activating user for activation key {}", key); 
                   return userRepository.findOneByActivationKey(key) 
                       .flatMap(user -> { 
                           // activate given user for the registration key. 
                           user.setActivated(true); 
                           user.setActivationKey(null); 
                           return saveUser(user); 
                       }) 
                       .doOnNext(user -> log.debug("Activated user: {}", user)); 
                 } 
                 [...] 
                } 

Para los servicios, también es necesario disponer de API responsivas para el código que necesita los datos de la capa repository.

c. La capa repository

La capa repository es diferente dependiendo de si una aplicación utiliza una base de datos SQL o una base de datos noSQL. Las primeras bases de datos que soportaron una API Reactive (Stream) fueron bases de datos noSQL. Llevó bastante tiempo disponer de una solución limpia para usar bases de datos SQL en modo responsivo con, por ejemplo, R2DBC. R2DBC

El uso de Spring Data simplifica los repositorios:

@SuppressWarnings("unused"@Repository 
                public interface UsuarioRepository extends  
                ReactiveN1qlCouchbaseRepository<Usuario, String> { 
                } 

Todo se tiene en cuenta. Spring Data gestiona completamente las consultas de acceso a datos estándar. No hay necesidad de codificarlos.

Podemos personalizar el repositorio:

@Repository 
                public interface UserRepository extends  
                ReactiveN1qlCouchbaseRepository<User, String> { 
                 Mono<User> findOneByActivationKey(String activationKey); 
                 Flux<User> findAllByActivatedIsFalseAndActivationKeyIsNotNullAnd 
                CreatedDateBefore(Instant dateTime); 
                 Mono<User> findOneByResetKey(String resetKey); 
                 Mono<User> findOneByEmailIgnoreCase(String email); 
                 default Mono<User> findOneByLogin(String login) { 
                   return findById(User.PREFIX + ID_DELIMITER + login); 
                 } 
                 Flux<User> findAllByLoginNot(Pageable pageable, String login); 
                 Mono<Long> countAllByLoginNot(String anonymousUser); 
                } 

d. Repository responsivo R2DBC

El proyecto R2DBC (Responsive Relational Database Connectivity) aporta las API de programación responsiva a las bases de datos relacionales. Se basa en la especificación Responsive Streams. R2DBC proporciona una API sin bloqueo totalmente responsiva.

Sitio web de R2DBC: https://r2dbc.io/

R2DBC funciona con bases de datos relacionales. A diferencia de lo que sucede con JDBC, que está bloqueando, R2DBC nos permite trabajar con bases de datos SQL utilizando una API responsiva. Esto permite pasar del modelo clásico de un thread por conexión a un enfoque más potente y escalable. R2DBC es una especificación abierta que establece una interfaz de proveedor de servicios (SPI) para los proveedores de controladores.

Por el momento, R2DBC trabaja sobre estas bases de datos:

Driver

Base de datos

cloud-spanner-r2dbc

driver for Google Cloud Spanner.

jasync-sql

R2DBC wrapper for Java & Kotlin Async Database Driver for MySQL and PostgreSQL (written in Kotlin). 

r2dbc-h2

native driver implemented for H2 as a test database.

r2dbc-mariadb

native driver implemented for MariaDB.

r2dbc-mssql

native driver implemented for Microsoft SQL Server.

r2dbc-mysql

native driver implemented for MySQL.

r2dbc-postgres

native driver implemented for PostgreSQL.

Oracle ha anunciado ojdbc20, que expondrá métodos con un Publisher, pero sabemos que, por el momento, la gestión asíncrona es mínima.

R2DBC proporciona un pool de conexiones responsivas: r2dbc-pool, y un sistema de observación a través de r2dbc-proxy.

Podemos usarlo solo o a través de Spring Data.

Solo para H2

Dependencia Maven:

<dependency> 
                   <groupId>io.r2dbc</groupId> 
                   <artifactId>r2dbc-spi</artifactId> 
                   <version>0.8.2</version> 
                   <scope>test</scope> 
                </dependency> 
                <dependency> 
                   <groupId>io.r2dbc</groupId> 
                   <artifactId>r2dbc-spi-test</artifactId> 
                   <version>0.8.2.RELEASE</version> 
                   <scope>test</scope> 
                </dependency> 
                <dependency> 
                   <groupId>io.r2dbc</groupId> 
                   <artifactId>r2dbc-h2</artifactId> 
                   <version>0.8.4.RELEASE</version> 
                   <scope>test</scope> 
                </dependency> 

Creación de una conexión factory:

@Bean 
                public ConnectionFactory  
                connectionFactory(R2DBCConfigurationProperties properties) { 
                   ConnectionFactoryOptions baseOptions =  
                ConnectionFactoryOptions.parse(properties.getUrl()); 
                   Builder ob = ConnectionFactoryOptions.builder().from(baseOptions); 
                   if (!StringUtil.isNullOrEmpty(properties.getUser())) { 
                       ob = ob.option(USER, properties.getUser()); 
                   } 
                   if (!StringUtil.isNullOrEmpty(properties.getPassword())) { 
                       ob = ob.option(PASSWORD, properties.getPassword()); 
                   } 
                   return ConnectionFactories.get(ob.build()); 
                } 

Propiedad para la conexión:

r2dbc:h2:mem://./testdb 

Para ejecutar una consulta en una DAO:

public Mono<Usuario> findById(long id) { 
                 return Mono.from(connectionFactory.create()) 
                   .flatMap(c -> Mono.from(c.createStatement( 
                            "select id,nom,prenom from Usuario where id = $1") 
                   .bind("$1", id) 
                   .execute()) 
                    .doFinally((st) -> close(c))) 
                   .map(result -> result.map((row, meta) -> 
                   new Usuario(row.get("id", Long.class), 
                     row.get("iban", String.class), 
                     row.get("balance", BigDecimal.class)))) 
                   .flatMap( p -> Mono.from(p)); 
                } 

Con Spring Data y H2

También podemos usar Spring Data directamente. Spring soporta la configuración de Spring con clases @Configuration basadas en Java para una instancia de controlador R2DBC. La clase de utilidad DatabaseClient aumenta la productividad durante la ejecución de operaciones R2DBC comunes al proporcionar un mapeo de objetos integrado entre las filas de base de datos y los POJO.

Hay una traducción de las excepciones en la jerarquía de excepciones de acceso a los datos portátiles de Spring. Se pueden utilizar el mapeo de objetos y los convertidores Spring.

Dependencias Maven:

<dependencyManagement> 
                 <dependencies> 
                   <dependency> 
                     <groupId>io.r2dbc</groupId> 
                     <artifactId>r2dbc-bom</artifactId> 
                     <version>${r2dbc-releasetrain.version}</version> 
                     <type>pom</type> 
                     <scope>import</scope> 
                   </dependency> 
                 </dependencies> 
                </dependencyManagement> 
                 
                <dependencies> 
                 <dependency> 
                   <groupId>org.springframework.data</groupId> 
                   <artifactId>spring-data-r2dbc</artifactId> 
                   <version>1.1.4.RELEASE</version> 
                 </dependency> 
                 
                 <!-- a R2DBC driver --> 
                 <dependency> 
                   <groupId>io.r2dbc</groupId> 
                   <artifactId>r2dbc-h2</artifactId> 
                   <version>0.8.4.RELEASE</version> 
                 </dependency> 
                </dependencies> 

Posicionamiento del log Spring Data:

logging.level.org.springframework.data.r2dbc=DEBUG 

Uso de ConnectionFactory:

ConnectionFactory connectionFactory =  
                ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_ 
                DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); 
                 
                DatabaseClient client =  
                DatabaseClient.create(connectionFactory); 
                 
                client.execute("CREATE TABLE usuario" + 
                   "(id VARCHAR(255) PRIMARY KEY," + 
                   "nom VARCHAR(255)," + 
                   "age INT)") 
                 .fetch() 
                 .rowsUpdated() 
                 .as(StepVerifier::create) 
                 .expectNextCount(1) 
                 .verifyComplete(); 
                 
                client.insert() 
                 .into(Usuario.class) 
                 .using(new Usuario("Pepe", 51)) 
                 .then() 
                 .as(StepVerifier::create) 
                 .verifyComplete(); 

Programación funcional

Es posible hacer programación funcional:

Flux<Usuario> usuario = databaseClient.select() 
                 .from(Usuario.class) 
                 .fetch() 
                 .all(); 

Repository Spring Data

El repositorio es como un repositorio clásico:

public interface PersonRepository extend 
                 ResponsiveCrudRepository<User, Long> { 
                 
                 } 

Cliente responsivo

Podemos usar la clase WebClient, que se introdujo en Spring 5 y es la equivalente del RestTemplate tradicional. Es un cliente sin bloqueo que soporta flujos responsivos. Permite recuperar datos de los endpoints proporcionados por el controlador WebFlux. La clase WebClient va a sustituir a RestTemplate, que quedará obsoleta en breve. WebClient RestTemplate

Vamos a crear un UsuarioWebClient sencillo:

public class UsuarioWebClient { 
                 
                   WebClient client = WebClient.create("http://localhost:8080"); 
                 
                   // ... 
                } 

Recuperación de un único recurso:

Mono<Usuario> usuarioMono = client.get() 
                 .uri("/usuarios/{id}", "1") 
                 .retrieve() 
                 .bodyToMono(Usuario.class); 
                 
                usuarioMono.subscribe(System.out::println); 

Recuperación de una colección:

Flux<Usuario> usuariosFlux = client.get() 
                 .uri("/usuarios") 
                 .retrieve() 
                 .bodyToFlux(Usuario.class); 
                employeeFlux.subscribe(System.out::println); 

Pruebas con WebFlux

Spring ofrece una serie de herramientas para facilitar las pruebas.

1. Pruebas unitarias

Las pruebas unitarias se corresponden con pruebas de bajo nivel.

a. Pruebas unitarias con aplicaciones responsivas

Podemos utilizar el tradicional Mockito.

Para una clase:

public class UsuarioService { 
                 public Mono<Usuario> getUsuarioById(Integer usuarioId) { 
                 return webClient 
                   .get() 
                   .uri("http://localhost:8080/usuario/{id}", usuarioId) 
                   .retrieve() 
                   .bodyToMono(Usuario.class); 
                 } 
                } 

Podemos tener la prueba unitaria:

@ExtendWith(MockitoExtension.class) 
                public class UsuarioServiceTest { 
                 
                 @Test 
                 void givenUsuarioId_whenGetUsuarioById_thenReturnUsuario() { 
                 
                 Integer usuarioId = 100; 
                 Usuario mockUsuario = new Usuario(1, "Pepe", 51); 
                 when(webClientMock.get()) 
                   .thenReturn(requestHeadersUriSpecMock); 
                 when(requestHeadersUriMock.uri("/usuario/{id}", usuarioId)) 
                   .thenReturn(requestHeadersSpecMock); 
                 when(requestHeadersMock.retrieve()) 
                   .thenReturn(responseSpecMock); 
                 when(responseMock.bodyToMono(Usuario.class)) 
                   .thenReturn(Mono.just(mockUsuario)); 
                 
                 Mono<Usuario> usuarioMono =  
                usuarioService.getUsuarioById(usuarioId); 
                 
                 StepVerifier.create(usuarioMono) 
                   .expectNextMatches(usuario -> usuario.getNombre() 
                   .equals("Pepe")) 
                   .verifyComplete(); 
                 } 
                } 

b. Uso de MockWebServer MockWebServer

MockWebServer fue desarrollado por el equipo Squaere, cuyo trabajo encontrará en el sitio web https://github.com/square/okhttp/tree/master/mockwebserver

MockWebServer es un servidor web que permite ejecutar scripts para probar clientes HTTP enviando solicitudes y verificando respuestas.

Se utiliza como mockito:

  • Codificar los mocks en script.

  • Ejecutar el código de la aplicación.

  • Verificar que el número de llamadas esperadas coincide con las que se ejecutaron.

Para usarlo, necesita las siguientes dependencias:

<dependency> 
                   <groupId>com.squareup.okhttp3</groupId> 
                  <artifactId>okhttp</artifactId> 
                   <version>4.9.0</version> 
                   <scope>test</scope> 
                </dependency> 
                <dependency> 
                   <groupId>com.squareup.okhttp3</groupId> 
                   <artifactId>mockwebserver</artifactId> 
                   <version>4.9.0</version> 
                   <scope>test</scope> 
                </dependency> 

Para nuestro UsuarioService:

public class UsuarioServiceMockWebServerTest { 
                 public static MockWebServer mockBackEnd; 
                 
                 @BeforeAll 
                 static void setUp() throws IOException { 
                   mockBackEnd = new MockWebServer(); 
                   mockBackEnd.start(); 
                 } 
                 
                 @AfterAll 
                 static void tearDown() throws IOException { 
                   mockBackEnd.shutdown(); 
                 } 
                } 

Mapeo de puertos:

@BeforeEach 
                void initialize() { 
                 String baseUrl = String.format("http://localhost:%s", 
                   mockBackEnd.getPort()); 
                 usuarioService = new UsuarioService(baseUrl); 
                } 

Llamada y recuperación:

@Test 
                void getUsuarioById() throws Exception { 
                 Usuario mockUsuario = new Usuario(1, "Pepe", 51,  
                Role.ADMIN); 
                 mockBackEnd.enqueue(new MockResponse() 
                   .setBody(objectMapper.writeValueAsString(mockUsuario)) 
                   .addHeader("Content-Type", "application/json")); 
                  Mono<Usuario> usuarioMono =  
                usuarioService.getUsuarioById(1); 
                  StepVerifier.create(usuarioMono) 
                   .expectNextMatches(usuario -> usuario.getRole() 
                   .equals(Role.ADMIN)) 
                   .verifyComplete(); 
                } 

2. Pruebas de integración

Las pruebas de integración consisten en probar con un contexto Spring parcial de pruebas que contienen una parte de la configuración. Todavía es posible escribir pruebas de integración que requieran un contexto de aplicación completo utilizando @SpringBootTest combinado con @AutoConfigureWebTestClient.

Uso de @WebFluxTest con WebTestClient WebTestClient

Se deben usar dependencias Maven:

<dependency> 
                   <groupId>io.projectreactor</groupId> 
                   <artifactId>reactor-test</artifactId> 
                   <scope>test</scope> 
                </dependency> 

El uso de la anotación @WebFluxTest deshabilita la configuración automática completa y aplica solo la configuración relevante en su lugar.

Las siguientes anotaciones están deshabilitadas: @Controller, @ControllerAdvice, @JsonComponent, @Converter y @WebFluxConfigurer.

Las siguientes anotaciones permanecen habilitadas: @Component, @Service y @Repository.

De forma predeterminada, las pruebas anotadas con @WebFluxTest también configurarán automáticamente un WebTestClient. El @WebFluxTest se utiliza en combinación con @MockBean o @Import para crear todos los colaboradores requeridos por los beans @Controller.

El cliente responsivo sin bloqueo WebTestClient se puede utilizar para probar los servidores web que utilizan el WebClient responsivo internamente. Esto permite ejecutar consultas y proporciona una API fluida para verificar las respuestas.

WebTestClient es similar a MockMvc. La única diferencia entre estos clientes web de prueba es que WebTestClient tiene como objetivo probar los endpoints WebFlux.

Por ejemplo, tendremos:

@ExtendWith(SpringExtension.class) 
                @WebFluxTest(controllers = UsuarioController.class) 
                @Import(UsuarioService.class) 
                public class UsuarioControllerTest 
                { 
                 @MockBean 
                 UsuarioRepository repository; 
                 
                 @Autowired 
                 private WebTestClient webClient; 
                 
                 @Test 
                 void testCreateUsuario() { 
                   Usuario usuario = new Usuario(); 
                   usuario.setId(1); 
                   usuario.setNombre("Pepe"); 
                   usuario.setEdad(51); 
                 
                   Mockito.when(repository.save(usuario)).thenReturn(Mono. 
                just(usuario)); 
                 
                   webClient.post() 
                     .uri("/usuario") 
                     .contentType(MediaType.APPLICATION_JSON) 
                     .body(BodyInserters.fromObject(usuario)) 
                     .exchange() 
                     .expectStatus().isCreated(); 
                 
                   Mockito.verify(repository, times(1)).save(usuario); 
                 } 
                 
                 @Test 
                 void testGetUsuarioById() 
                 { 
                   Usuario usuario = new Usuario(); 
                   usuario.setId(1); 
                   usuario.setNombre("Pepe"); 
                   usuario.setEdad(51); 
                 
                   Mockito 
                     .when(repository.findById(100)) 
                     .thenReturn(Mono.just(usuario)); 
                 
                   webClient.get().uri("/usuario/{id}", 1) 
                     .exchange() 
                     .expectStatus().isOk() 
                     .expectBody() 
                     .jsonPath("$.nombre").isNotEmpty() 
                     .jsonPath("$.id").isEqualTo(1) 
                     .jsonPath("$.nombre").isEqualTo("Pepe") 
                     .jsonPath("$.edad").isEqualTo(51); 
                 
                   Mockito.verify(repository, times(1)).findById(100); 
                 } 
                } 

Una variante con jUnit5 y @SpringBootTest:

@ExtendWith(SpringExtension.class) 
                @SpringBootTest(webEnvironment =  
                SpringBootTest.WebEnvironment.RANDOM_PORT) 
                public class SampleHandlerTest { 
                   @Autowired 
                   private ApplicationContext applicationContext; 
                 
                   private WebTestClient webTestClient; 
                 
                   @org.junit.jupiter.api.Test 
                   public void test_post() { 
                       webTestClient =  
                WebTestClient.bindToApplicationContext(applicationContext).configure 
                Client().responseTimeout(Duration.ofHours(1)).build(); 
                 
                       Usuario usuario = new Usuario("Pepe", 51); 
                 
                       webTestClient.post().uri("/usuario/1") 
                               .header(HttpHeaders.CONTENT_TYPE, 
                MediaType.APPLICATION_JSON_VALUE) 
                               .accept(MediaType.APPLICATION_JSON) 
                               .contentType(MediaType.APPLICATION_JSON) 
                               .body(Mono.just(usuario), Usuario.class) 
                               .exchange().expectStatus().isOk(); 
                   } 
                } 
VM6883:64

Server Site Event con Spring

En nuestras aplicaciones, a menudo se requieren refrescos para llamadas asíncronas. Hay varias tecnologías para esto. La más sencilla es el pooling, que consiste en consultar regularmente al servidor para conocer el avance de un procesamiento. Otra evolución es el long pooling, que consiste en mantener abierta la conexión HTTP; pero estas dos técnicas son costosas a nivel de red. Existe una evolución con webhooks y Publish-Subscribe. También tenemos los websockets, que son bidireccionales, y Server Site Event o SSE, para abreviar, que es un estándar HTTP que permite a una aplicación web manejar un flujo unidireccional de eventos y recibir actualizaciones cada vez que el servidor emite datos. Esta solución suele ser más adecuada en un contexto web que no necesite la característica en tiempo real de los websockets. Por lo tanto, perdemos algo de tiempo, pero algunas veces esto es sostenible.

La versión Spring 4.2 ya soportaba SSE, pero, a partir de Spring 5, hay una forma más idiomática y práctica de gestionarlo a través de un flujo y un elemento ServerSiteEvent, gracias a las API responsivas de Spring WebFlux. Creamos un endpoint de streaming SSE, siguiendo las especificaciones del W3C que designan el tipo MIME como text/event-stream:

@GetMapping(path = "/stream-flux", produces  
                =MediaType.TEXT_EVENT_STREAM_VALUE) 
                public Flux<String> streamFlux() { 
                 return Flux.interval(Duration.ofSeconds(1)) 
                  .map(sequence -> "Flux - " + LocalTime.now().toString()); 
                } 

El método Flux.interval(..) crea un flujo que emite valores de manera incremental. A continuación, asignamos estos valores al formato de salida deseado, encapsulándolo en un objeto ServerSentSevent:

@GetMapping("/stream-sse") 
                public Flux<ServerSentEvent<String>> streamEvents() { 
                 return Flux.interval(Duration.ofSeconds(1)) 
                 .map(sequence -> ServerSentEvent.<String> builder()  
                   .id(String.valueOf(sequence))  
                   .eventname("periodic-event")  
                   .payload("Mis datos") 
                   .build()); 
                } 

Los datos transmitidos están en formato de texto. Es posible serializar/deserializar en JSON, XML, etc. También es posible enviar solo cambios de un objeto mediante el uso de actualizaciones incrementales (incremental upgrade) con JSON-PATCH (RFC-6902) RFC: https://datatracker.ietf.org/doc/html/rfc6902, sitio: http://jsonpatch.com/

Para usarlo con Maven:

<dependency> 
                   <groupId>com.github.java-json-tools</groupId> 
                   <artifactId>json-patch</artifactId> 
                   <version>1.12</version> 
                </dependency> 

Tiene que pensar en gestionar la posible pérdida de una actualización.

Todo lo que queda es consumir el flujo en un WebClient:

public void ConsumirelflujoSSE() { 
                   WebClient client = WebClient.create("http://localhost:8080/ 
                sse-server"); 
                   ParameterizedTypeReference<ServerSentEvent<String>> type 
                    = new ParameterizedTypeReference<ServerSentEvent<String>>() {}; 
                   Flux<ServerSentEvent<String>> eventStream = client.get() 
                     .uri("/stream-sse") 
                     .retrieve() 
                     .bodyToFlux(type); 
                   eventStream.subscribe( 
                     content -> logger.info("Time: {} - event: name[{}], id [{}], 
                                  content[{}] ", 
                                  LocalTime.now(), content.event(), content.id(), 
                                  content.data()), 
                                  error -> logger.error("Error {}", error), 
                                          () -> logger.info("Completed!!!")); 
                } 

Así tenemos la generación de un flujo de eventos que se corresponde con los mensajes enviados por nuestros clientes. Aquí vemos que podemos hacer una cadena de servidores responsivos con comunicaciones bidireccionales. Esto permite realizar largos procesos responsivos asíncronos con una respuesta inmediata, lo que significa que la solicitud de procesamiento se ha tenido en cuenta con una notificación propagable que indica el avance y el final del procesamiento con, si se desea, el resultado del procesamiento en payload del mensaje de final de procesamiento.

Para ir más allá

La adopción de aplicaciones responsivas es muy lenta. A priori, tenemos los mismos obstáculos que para el uso de Domain Driven Design, arquitecturas hexagonales, Event Sourcing, etc. Por el momento, a menudo se considera más fácil trabajar de la manera tradicional, incluso si esto implica más servidores.

Spring proporciona un conjunto completo, que funciona con Java y Kotlin. Los servidores más innovadores están en Kotlin, utilizan API responsivas y funcionan con Flux en el Cloud. Las aplicaciones responsivas con frecuencia usan bases de datos Kafka y Cassandra y están basadas en eventos.

El número de servidores se convierte en un criterio importante cuando se utiliza un Cloud de Amazon, Google, Azure u otra nube porque se busca reducir el número de máquinas.

Puntos clave

  • La adopción de aplicaciones responsivas es lenta.

  • Reactor es un motor que permite programar aplicaciones responsivas.

  • WebFlux es el equivalente de Spring MVC en el mundo responsivo.

  • WebFlux soporta la backpressure.

  • Para crear un servicio web responsivo, necesita una base de datos que soporte las API responsivas.

  • Spring Data se puede utilizar para repositorios responsivos.

  • R2DBC se puede utilizar para bases de datos SQL responsivas soportadas.

Introducción JHipster

Para ilustrar un uso avanzado de Spring, veremos algunos elementos de configuración de un proyecto JHipster.

JHipster es un generador de aplicaciones interactivas. Se basa en Spring Boot y Angular con Yeoman, Maven o Gradle. En este capítulo, veremos la parte back de la aplicación y la parte front con Angular. JHipster está evolucionando muy rápidamente, ya que se utiliza mucho y tiene un gran número de colaboradores que contribuyen. Angular

La principal particularidad de este generador es que es open source y que puede modificar fácilmente las plantillas de generación para generar sus propias aplicaciones. Es muy popular y los frameworks que utiliza lo aprovechan para mejorar, tomándolo como vector de mejora y como demostrador. La herramienta JHipster y sus plantillas están escritas en JavaScript.

Solo veremos algunas partes porque esta herramienta, que está orientada a DevOps, es muy modular y cubre muchas combinaciones de tecnologías.

La herramienta JHipster es personalizable, pero esta personalización solo es necesaria si desea crear varias aplicaciones. Para un solo proyecto, hay dos puntos de vista opuestos: comenzar desde cero o partir del código generado por un generador. Siempre es interesante estudiar este código generado para listar los problemas y nada impide personalizarlo. Comenzar desde cero en aplicaciones con este nivel de dificultad técnica solo es posible si ya tiene una base sólida y si dispone de una cantidad considerable de tiempo (con su presupuesto asociado). A mi modo de ver, esta herramienta es una ventaja estratégica para explorar nuevas tecnologías.

Aspectos generales

JHipster es una herramienta creada en 2014 por Julien DUBOIS, experto en Spring y Java, entre otros.

Las estadísticas muestran más de 168 000 descargas por mes, 19 335 estrellas en GitHub y más de 800 colaboradores. Más de 300 empresas utilizan oficialmente JHipster. El presupuesto de sponsoring es cercano a los 50 000 $ anuales y se utiliza para financiar costes de alojamiento, los bugs Bounties, correcciones de bugs o evoluciones importantes y complejos.

Está totalmente registrado bajo licencia open source y colaborativa, y la URL del proyecto es https://www.jhipster.tech/. Consiste en un proyecto principal y subproyectos asociados.

El objetivo del proyecto es ayudar a generar una aplicación full stack en un mínimo tiempo, con un sistema de plantillas y preguntas/respuestas. Los aspectos full stacks incluyen el backend basado en Springboot, la base de datos y la parte front, que se basa en SPA (Single Page Applications) con Angular, React o View. También incluye el lado de producción con la posibilidad de preparar entornos cloud y dockerizados. Asimismo, incluye pruebas y, por lo tanto, proporciona una vista de end to end del proyecto.

La adopción de la herramienta por una gran comunidad ha permitido definir una serie de buenas prácticas. La herramienta JHipster está disponible en línea de comandos y como una aplicación web.

La línea de comandos permite hacer todo y es posible crear scripts. La versión web es una versión simplificada que se utiliza bastante para comenzar.

1. JHipster web

A continuación, se muestra un ejemplo sencillo con la versión en línea, disponible en https://start.jhipster.tech/

Esta versión es gratuita y open source. Es muy posible modificarla para ponerla a disposición de los desarrolladores de una empresa, personalizando las plantillas.

images/19EP01N.png

Primero se debe registrar y después se le dirigirá a la página principal.

images/19EP02N.png

El proyecto se genera en dos pasos:

  • Creación de un esqueleto de aplicación vacío.

  • Adición de entidades de negocio.

Podemos pedirle a la herramienta que aloje los archivos fuente generados en GitHub o GitLab.

Posteriormente, hacemos clic en Create application y elegimos la configuración de nuestra aplicación:

Parámetro

Valor

Application name

demo

Repository name

demo

Application type

Monolithic application

Default Java package name

fr.eni

Port

8080

Use the JHipster Registry

No

Authentication

JWT

Database type

H2

Prod database

MySQL

Dev development

H2 with disk-based persistence

Spring cache abstraction

Yes

Hibernate 2nd level cache

Yes

Maven or Gradle

Maven

Framework client

Angular

Internationalization

No

Tests en plus

Protractor

La versión Monolith permite tener una aplicación completa en la misma aplicación; la opción de microservicios permite separar los diferentes componentes de la aplicación en diferentes proyectos, compuestos por varios servicios agregados con un gateway.

Hay un generador principal y un marketplace que ofrece extensiones: https://www.jhipster.tech/modules/marketplace/#/list

Podemos modificar las plantillas del generador del proyecto principal. También podemos modificar los modelos creando de módulos o blueprint que son módulos más elaborados.

Para las pruebas, JHipster permite generar código y la configuración para:

  • JUnit y Karma

  • Gatling

  • Cucumber

  • Protractor

Generamos el proyecto desde la página web JHipster y recuperamos el zip.

Las fuentes generadas también contienen archivos para ayudarnos a configurar git en nuestro proyecto y archivos de configuración que permiten que las herramientas de desarrollo comunes se autoconfiguren.

Tenemos un proyecto Maven estructurado como un proyecto clásico de Maven. En Java, tenemos una aplicación Springboot con:

  • configuración de esqueleto vacío,

  • configuración predefinida para el proyecto,

  • seguridad,

  • Jackson para json.

El front es opcional en JHipster. Podemos elegir un framework para el front, como Angular, y utilizar después el administrador de paquetes npm. El proyecto Typescript Angular se genera en la web/app.

Pantallas de herramientas:

  • Homepage

  • Seguridad

Lanzamos la aplicación Angular en modo desarrollo, por lo que tenemos una aplicación compuesta por los siguientes elementos:

  • Páginas de ayuda.

  • Navegador generado por Webpack con Hot reload.

  • El logotipo es aleatorio (4 actualmente).

  • Pantallas de administración de Springboot micrometer.

  • Swagger: aplicación REST de la aplicación back.

La hot reload está en las partes front y back. La aplicación se reinicia automáticamente en las partes front y back, lo que permite ver de forma inmediata los impactos de una modificación en las fuentes de la aplicación.

Browsersync permite realizar pruebas en varios navegadores simultáneamente para visualizar el resultado, por ejemplo, entre una versión web y una versión móvil en dos ventanas separadas, y esto con la aplicación de cambios directamente después de guardar y recompilar de forma automática las fuentes.

Encontramos pruebas end 2 end con Protractor (en el directorio e2e). Estos lanzan un navegador Chrome y realizan clics en la aplicación, lo que permite llevar a cabo pruebas de integración completas.

A continuación, podemos configurar y generar la parte de negocio. Esta configuración se puede realizar de forma interactiva desde la línea de comandos, con un conjunto de preguntas y respuestas, o usando un archivo JDL que contiene la descripción del modelo.

JHipster proporciona una herramienta de desarrollo en línea y un plugin para Eclipse, que permiten manipular este archivo JDL con un DSL (Domain Specific Language). En las estadísticas (https://start.jhipster.tech/statistics) podemos observar más de 37 000 JDL generadas. Algunos proyectos que utilizan esta herramienta tienen varios cientos de entidades.

images/19EP03N.png

A continuación, la herramienta genera las entidades de negocio y toda la parte SCRUD correspondiente.

Con fakerjs, podemos generar un conjunto de datos predeterminado para probar pantallas. Estos datos se basan en csv personalizables.

Más adelante veremos algunos elementos sobre las opciones de arquitectura que se han tomado en las plantillas estándares de JHipter, pero, en primer lugar, debemos abordar algunos puntos.

2. Personalización de la herramienta JHispter

Cuando proponemos el uso de JHipster para un proyecto, a menudo se nos dice que la herramienta genera una aplicación que no se corresponde con lo que habríamos hecho si la hubiéramos codificado nosotros mismos desde cero. Esta afirmación es interesante porque no es la herramienta la que se critica, sino las plantillas utilizadas. Encontramos esta crítica con las fuentes generadas por el generador OpenAPI-generator y, más generalmente, con todos los generadores de código.

Cuando hablamos de personalizarlas, a menudo se nos responde que nuestro trabajo no es crear y desarrollar plantillas.

Hoy en día, algunos equipos prefieren un aumento gradual de las habilidades y competencias codificando todo a mano. Esto es aún más cierto en las empresas, que no tienen un núcleo de arquitectura que se pudiera encargar de desarrollar estas plantillas. Esta es una posición defensiva. Por lo general, el equipo ya tiene que desarrollar habilidades y competencias en las partes front y back, en los aspectos de devops, y después luchar para tener éxito en la creación de una aplicación que funcione. El problema con las plantillas es que tendría que conocer la arquitectura técnica completa de su proyecto desde el principio. De hecho, el momento de la implementación es lo crítico.

Un escenario facilitador para la adopción del producto JHipster es dividir el proyecto empresarial en fases.

Una primera fase consiste en hacer un prototipo del proyecto a mano para aprender y familiarizarse con las tecnologías utilizadas. Podemos comparar nuestras elecciones con las realizadas en las aplicaciones de ejemplo generadas con JHipster. Después de todo, no hay una opción mejor que otra. Este prototipo puede tardar algún tiempo en codificarse y también se debe codificar las pruebas. En cualquier caso, solo se puede automatizar algo cuando se entiende y ya funciona sin automatización.

En algún momento del proyecto, nos daremos cuenta de que hay patrones reutilizados en el código y que se podría generar la mayor parte del código, y es entonces cuando podemos usar JHipster en todo su potencial. Todo lo que tiene que hacer es modificar las plantillas y, si es necesario, el generador.

Hay que saber que el sistema de plantillas y el generador son muy sencillos, en comparación con los aspectos técnicos que es necesario conocer, para codificar una aplicación empresarial full stack que haga SPA. Hay algunos conceptos técnicos básicos que es preciso aprender, pero se deben comparar con el tiempo necesario para reescribir código una y otra vez. Las plantillas son como macros.

Otro escenario consiste en volver a empezar desde una aplicación que funcione y usarla como plantilla. Tendremos una parte fija que representa la infraestructura de nuestro proyecto y otra variable que se corresponde con las entidades de negocio del proyecto. La parte fija es la que mueve muy poco de un proyecto a otro, y la parte variable, es la que se corresponde con la parte de negocio que más cambia.

En cualquier caso, puede ser interesante pensar a nivel global en las arquitecturas utilizadas con el fin de evitar tener aplicaciones demasiado diferentes que se comunican mal y problemas para gestionar las habilidades y competencias de los desarrolladores que se mueven de un proyecto a otro.

3. Niveles de personalización

Hay varios niveles de modificación del generador:

  • Solo modificaciones en las plantillas.

  • Modificación con adición de archivos.

  • Modificaciones en los metadatos de las entidades generadas.

La herramienta JHispter genera un modelo de la aplicación a partir de una JDL (o mediante preguntas y respuestas).

Este modelo es un árbol de objetos que corresponde a las entidades con sus atributos. A partir de esta plantilla, se utilizan juegos de plantillas, que llamaremos paquetes o packs. Si la plantilla JDL es suficiente, entonces nuestras modificaciones serán sencillas. Si tenemos que complicar la estructura del modelo que se corresponde con el formato del archivo JDL, entonces las modificaciones son más complejas, pero factibles.

La herramienta JHipster tiene una orientación MDA (Model Driven Architecture). Partimos de la descripción de un modelo de datos para construir una aplicación. Cabe señalar que también es posible tener un enfoque API First, pero esto va más allá del alcance de estas primeras explicaciones sobre la herramienta.

En general, solo crearemos un subgenerador JHipster como módulo. Para modificaciones más complejas, haremos un módulo o un blueprint y, para una modificación mayor, podemos hacer un fork del generador principal, en orden de complejidad.

Podemos encontrar paquetes estándares en el directorio del generador: generators.

Generadores principales

Paquete

Utilidad

app

Crea una nueva aplicación JHipster basada en las opciones seleccionadas (submódulos jhipster: client, jhipster: server y jhipster: languages).

aws-containers

Inicializa una aplicación AWS y genera un contenedor Docker listo para transferirse a AWS.

aws

Inicializa una aplicación de AWS y genera un archivo JAR listo para transferirse a AWS.

azure-app-service

Despliega una aplicación en Azure App Service.

azure-spring-cloud

Despliega una aplicación en Azure Spring Cloud.

ci-cd

Crea scripts de pipeline para varias herramientas CI/CD, en función de las opciones seleccionadas.

cliente

Crea una nueva aplicación del lado cliente JHipster, basada en las opciones seleccionadas.

CloudFoundry

Genera una carpeta deploy/cloudfoundry con un archivo manifest.yml específico para desplegar en Cloud Foundry. Añade la dependencia spring-cloud-cloudfoundry-connector a Maven/Gradle si corresponde.

common

Parte común: archivos .gitignore, .prettierrc, etc.

cypress

Pruebas end to end cypress JavaScript (https://www.cypress.io/).

database-changelog-liquibase

Gestión de los changelogs de liquibase y de los fakedata csv.

database-changelog

Changelogs sobre las entidades.

docker-compose

Crea toda la configuración de despliegue Docker necesaria para las aplicaciones seleccionadas: docker-compose.yml y, si está utilizando la detección y la configuración de servicio: jhipster-registry.yml o consul.yml, central-server-configuration/application.yml.

entity-client

Áreas comunes y específicas Angular, React y Vue.

entity-i18n

Gestión del i18n para la parte cliente.

entity-server

Parte del servidor: dominio, repositorio, servicio y web.

entity

Crea una nueva entidad JHipster: entidad JPA, componentes del lado del servidor Spring y componentes del lado cliente.

export-jdl

Crea el archivo JDL a partir de las entidades json.

gae

Inicializa una aplicación Google App Engine y genera un archivo JAR listo para enviarlo a Google App Engine. Añade plugins y dependencias a Maven/Gradle según corresponda.

heroku

Inicializa una aplicación Heroku y genera un archivo JAR listo para ser enviado a Heroku. Añade la dependencia spring-cloud-heroku-connector a Maven/Gradle si corresponde.

info

Muestra información sobre nuestro proyecto y nuestro sistema actual.

Comando d jhipster info

Conveniente tener contexto para el soporte.

kubernetes-helm

Crea todos los paquetes de administración necesarios para que las aplicaciones seleccionadas se implementen en Kubernetes.

kubernetes-knative

Crea toda la implementación de Kubernetes necesaria y la configuración de Knative para las aplicaciones seleccionadas.

kubernetes

Crea todas las configuraciones de despliegue de Kubernetes necesarias para las aplicaciones seleccionadas.

[application] / *. yml

languages

Selecciona idiomas de una lista de idiomas disponibles. Los archivos i18n se copiarán en la carpeta /webapp/i18n.

openapi-client

Genera código de cliente Java a partir de una definición OpenAPI/Swagger (API-First).

openshift

Crea toda la configuración de despliegue OpenShift necesaria para las aplicaciones seleccionadas.

page

Paquete utilizado para el cliente vue.

server

Crea una nueva aplicación JHipster del lado del servidor basada en las opciones seleccionadas.

spring-controller

Crea un nuevo controlador sencillo ressort REST MVC.

spring-service

Crea un nuevo servicio JHipster: Se trata de un sencillo bean de servicio Spring transaccional.

Cada uno de estos paquetes permite generar una parte de la aplicación.

Por ejemplo, jhipster/generator-jhipster/tree/master/generators/entity-server contiene las plantillas para generar las entidades de la capa de dominio. Se trata de plantillas EJS (Embedded JavaScript Templating).

Las plantillas se basan en el gráfico de objetos y generan código con los ifs, iterando en listas de entidades, variables en las entidades, etc.

Por ejemplo, para crear un getter:

  public String get<%= fieldInJavaBeanMethod %>ContentType() { 
                     return <%= fieldName %>ContentType; 
                  } 

Las plantillas son complejas porque tienen que gestionar todos los casos. Si las personalizamos, lo podemos simplificar usando solo nuestros casos de uso.

Por ejemplo, el generador JHipster Kotlin, que se puede encontrar en la dirección https://github.com/jhipster/jhipster-kotlin/tree/master/generators, solo personaliza ciertos paquetes, pero sigue siendo compatible con un máximo de opciones:

  • app

  • entity-server

  • server

  • spring-controller

  • spring-service

Vimos que necesitábamos disponer una aplicación blanca que servirá de modelo para nuestras plantillas de generación.

Para personalizar las plantillas, es necesario:

  • tener un modelo, una aplicación blanca,

  • definir sus objetivos,

  • crear un módulo, un blueprint o modificar el generador.

El módulo es más simple que un blueprint y ofrece menos personalización.

Crear un blueprint

La documentación oficial está en la dirección: https://www.jhipster.tech/modules/creating-a-blueprint

Un blueprint de JHipster es un generador Yeoman que se compone a partir de un subgenerador JHipster específico para ampliar la funcionalidad de este último. El blueprint puede sustituir al getter definido en el subgenerador y proporcionar sus propios modelos y funcionalidades. Los blueprints JHipster se enumeran en el marketplace JHipster con la etiqueta jhipster-blueprint. Esto permite crear blueprints de terceros, que pueden reemplazar una parte específica de JHipster.

Para usar un blueprint, ejecute el siguiente comando:

jhipster --blueprint <nombre del blueprint> 

Se debe recordar que un blueprint JHipster tiene las siguientes características:

  • Es un paquete NPM y es un generador Yeoman.

  • Sigue una extensión de las reglas Yeoman enumeradas en https://yeoman.io/generators/ y se puede instalar, usar y actualizar usando el comando yo. En lugar de utilizar el prefijo generator-, se utiliza generator-jhipster- y, en lugar de tener solo la palabra clave yeoman-generator debe tener dos palabras clave, a saber, yeoman-generator y jhipster-blueprint.

Un blueprint solo puede extender los siguientes subgeneradores (en la carpeta generadores):

  • common

  • client

  • server

  • entity

  • entity-client

  • entity-server

  • entity-i18n

  • languages

  • spring-controller

  • spring-service

Si quiere modificar otro pack, tiene que forkear el generador generator-jhipster. Un fork es más complejo de mantener en comparación con las evoluciones de las versiones de JHipster.

1. Blueprint para usar lombok en el dominio

Para crear un blueprint, podemos utilizar el generador de blueprint JHipster o clonar y modificar un blueprint existente tomando, por ejemplo, el blueprint jhipster-kotlin o el blueprint jhipster-nodejs. Para encontrar un proyecto en el que aplicarlo, puede enumerar las extensiones del marketplace https://www.jhipster.tech/modules/marketplace/#/list y filtrar por las etiquetas yeoman-generator jhipster-blueprint, JHipster-5/6/7 o buscar en Github con el criterio «org:jhipster jhipster blueprint».

Por lo tanto, también podemos usar el generador de blueprints JHipster (https://github.com/jhipster/generator-jhipster-blueprint) para ayudarnos a inicializar nuestro blueprint.

Instalación del generador de blueprint:

npm install -g generator-jhipster-blueprint 
                mkdir my-blueprint && cd my-blueprint 
                yo jhipster-blueprint 

Elegimos el subgenerador entity-server.

Un blueprint JHipster debe tener el generator-jhipster como dependencia y debe importar el subgenerador apropiado para sustituirlo.

const chalk = require('chalk'); 
                const EntityServerGenerator = require('generator-jhipster/generators/ 
                entity-server'); 
                const utils = require('generator-jhipster/generators/utils'); 
                module.exports = class extends EntityServerGenerator { 
                  constructor(args, opts) { 
                    super(args, Object.assign({ fromBlueprint: true }, opts)); // 
                fromBlueprint variable is 
                important 
                 
                    const jhContext = this.jhipsterContext =  
                this.options.jhipsterContext; 
                    if (!jhContext) { 
                      this.error(`This is a JHipster blueprint and should be used only 
                like ${chalk.yellow('jhipster --blueprint generator-jhipster-sample- 
                blueprint')}`); 
                    } 
                    this.configOptions = jhContext.configOptions || {}; 
                    utils.copyObjectProps(this, opts.context); 
                    if (jhContext.databaseType === 'cassandra') { 
                      this.pkType = 'UUID'; 
                    } 
                  } 
                 
                  get writing() { 
                    return super._writing(); 
                  } 
                }; 

Cualquier método que comience con un carácter de subrayado (underscore o _) se puede reutilizar desde la superclase que se está extendiendo; por ejemplo, EntityServerGenerator en el ejemplo anterior.

Cada subgenerador JHipster se compone de varias fases Yeoman; cada fase es un getter, get inicialization, por ejemplo. Un blueprint puede personalizar una o más fases del subgenerador al que sustituye.

Hay varias formas de personalizar una fase de JHipster:

1) Dejar que JHipster administre una fase; el blueprint no sustituye nada.

    get initializing() { 
                        return super._initializing(); 
                    } 

2) Sustituir toda la fase; es en este momento cuando el blueprint toma el control de una fase.

    get initializing() { 
                        return { 
                            myCustomInitPhaseStep() { 
                                // Do all your stuff here 
                            }, 
                            myAnotherCustomInitPhaseStep(){ 
                                // Do all your stuff here 
                            } 
                        }; 
                    } 

3) Sustituir parcialmente una fase; es en este momento cuando el blueprint obtiene la fase de JHipster y la personaliza.

    get initializing() { 
                        const phaseFromJHipster = super._initializing(); 
                        const myCustomPhaseSteps = { 
                            displayLogo() { 
                                // override the displayLogo method from the _initializing 
                phase of JHipster 
                            }, 
                            myCustomInitPhaseStep() { 
                                // Do all your stuff here 
                            }, 
                        } 
                        return Object.assign(phaseFromJHipster, myCustomPhaseSteps); 
                    } 

4) Decorar una fase; es en este momento cuando el blueprint ejecuta etapas personalizadas antes o después de la fase que proviene de JHipster.

    // Run the blueprint steps before and/or after any parent steps 
                    get initializing() { 
                        const customPrePhaseSteps = { 
                            myCustomPreInitStep() { 
                                // Stuff to do BEFORE the JHipster steps 
                            } 
                        }; 
                        const customPostPhaseSteps = { 
                            myCustomPostInitStep() { 
                                // Stuff to do AFTER the JHipster steps 
                            } 
                        }; 
                        return { 
                 
                            ...customPrePhaseSteps, 
                            ...super._initializing(), 
                            ...customPostPhaseSteps 
                        }; 
                    } 

También puede acceder a las variables y funciones de JHipster directamente desde un blueprint.

Puede acceder a la configuración .yo-rc.json, que incluirá tanto la configuración de JHipster como la configuración de su blueprint.

Puede usar constantes en las constantes del generador: https://github.com/jhipster/generator-jhipster/blob/master/generators/generator-constants.js

Por ejemplo:

    const javaDir = `${jhipsterConstants.SERVER_MAIN_SRC_DIR +  
                this.packageFolder}/`; 
                    const resourceDir = jhipsterConstants.SERVER_MAIN_RES_DIR; 
                    const webappDir = jhipsterConstants.CLIENT_MAIN_SRC_DIR; 

Puede utilizar todas las funciones en Generator-Base, en la dirección: https://github.com/jhipster/generator-jhipster/blob/master/generators/generator-base.js

Por ejemplo:

    this.angularAppName = this.getAngularAppName(); 
                // get the Angular application name. 
                    this.printJHipsterLogo(); // to print the JHipster logo 

Las funciones generator-base.js y las variables de generator-constant.js forman parte de la API pública y, por lo tanto, seguirán el control de versiones semver. Pero otros archivos, como generator-base-private.js, utils.js etc., no lo seguirán y podrían romper la firma del método entre las versiones menores.

2. Ejecución del blueprint local

Relacione su blueprint global a través del comando:

cd my-blueprint 
                npm link 

En caso de fork del generador:

cd generator-jhipster 
                npm link 
                 
                cd my-blueprint 
                npm link generator-jhipster 

Cree una nueva carpeta para la aplicación que hay que gestionar y asocie JHipster y su blueprint con ella:

mkdir my-app && cd my-app 
                 
                npm link generator-jhipster-myblueprint 
                npm link generator-jhipster (Optional: Needed only if you are using 
                a non-released JHipster version) 
                jhipster -d --blueprint myblueprint 

Para que su blueprint esté disponible en el mercado JHipster, se debe asegurar de que tiene las dos palabras yeoman-generator-claves y jhipster-blueprint en su npm publicado package.json. Una vez que publique su blueprint en npm, estará disponible en el marketplace.

JHipster como herramienta multitecnologías

La herramienta JHipster es una muy buena herramienta para probar tecnologías y ver cómo se relacionan entre sí. Intenta mostrar el estado del arte y las mejores prácticas; JHipster soporta muchos frameworks y tecnologías.

En su versión 7, soporta las siguientes tecnologías:

1. Lado del cliente

Lado del cliente

Funcionalidades

HTML5

HTML5 es la última revisión importante de HTML (un formato de datos diseñado para representar páginas web).

CSS3

Hojas de estilo en cascada versión 3.

Bootstrap

Colección de herramientas para la creación del diseño de sitios web y aplicaciones web.

TypeScript

Un lenguaje de programación que mejora y asegura la producción de código JavaScript.

Angular Angular

Angular es un framework JavaScript para desarrollar páginas web.

React

React es una librería JavaScript para facilitar la creación de aplicaciones web monopage, a través de la creación de componentes dependientes de un estado, que generan una página HTML (o porción) con cada cambio de estado.

Vue

Framework JavaScript para crear interfaces de usuario.

Redux

Redux es un contenedor de estado predecible para aplicaciones JavaScript.

JQuery

JQuery es una librería JavaScript para facilitar el scripting del lado del cliente en el código HTML de las páginas web.

Websocket

WebSocket es un protocolo de red web basado en canales de comunicación full-duplex a través de una conexión TCP.

Yarn

Yarn almacena en caché cada paquete instalado para que ya no tenga que descargarlos.

Webpack

Webpack recupera las dependencias y módulos JavaScript, entre otros, para generar archivos estáticos.

Gulp

Gulp es una herramienta para automatizar las tareas del flujo de trabajo de desarrollo.

Sass

Sass (Syntactically Awesome Stylesheets) es un lenguaje de generación de hojas de estilo.

Browsersync

Browsersync sincroniza las URL, interacciones y cambios de código en un navegador para varios dispositivos.

Test

Herramienta de prueba JavaScript (https://jestjs.io/)

Protractor

Protractor es un framework de pruebas end to end para aplicaciones Angular y AngularJS. Angular

2. Lado del servidor

Lado del servidor

Funcionalidades

Spring Boot

Spring Boot permite crear fácilmente aplicaciones Spring autónomas, que se pueden desplegar y ejecutar de manera sencilla en producción.

Spring Security

Spring Security es un framework que se concentra en la autenticación y autorización de aplicaciones Java.

Netflix OSS

Netflix Open Source Software es la (o una) librería open source de Netflix.

Consul

Consul simplifica el registro de servicios y el descubrimiento de otros servicios a través de una interfaz DNS o HTTP.

Gradle

Gradle es un motor de producción que se ejecuta en la plataforma Java. Permite construir proyectos en Java, Scala, Groovy o incluso C ++.

Maven

Maven gestiona la construcción, el reporting y la documentación de un proyecto a partir de información centralizada.

Hibernate

Hibernate es un framework open source que admite la persistencia de objetos en bases de datos relacionales.

Liquibase

Liquibase es una librería para el seguimiento, la administración y la aplicación de cambios en el esquema de la base de datos.

MySQL

MySQL es un sistema de gestión de bases de datos relacionales (RDBMS).

MariaDB

MariaDB es un sistema de gestión de bases de datos, fork comunitaria de MySQL.

PostgreSQL

PostgreSQL es un sistema de gestión de bases de datos relacionales y objetos (RDBMS).

Oracle

Oracle es un sistema de gestión de bases de datos relacionales (RDBMS).

SQL Server

SQL Server es un sistema de administración de bases de datos relacionales (RDBMS).

MongoDB

MongoDB es un sistema de gestión de bases de datos orientado a documentos (NoSQL).

Couchbase

Couchbase gestiona el acceso concurrente a los documentos, escalado (escalados sucesivos debido al uso cada vez más intenso a lo largo del tiempo), replicación, balanceado de carga, conmutación por error, copias de seguridad, etc.

Cassandra

Cassandra es un sistema de gestión de bases de datos (SGBD) demtipo NoSQL para cantidades masivas de datos.

Neo4j

Neo4j es un sistema de gestión de bases de datos open source basado en gráficos, desarrollado en Java.

EhCache

EhCache soporta la alta disponibilidad y replicación de datos de aplicaciones cacheadas y se usa a menudo con Hibernate.

Caffeine

Librería de caché de alto rendimiento para Java.

Hazelcast

Hazelcast es una cuadrícula de datos en memoria basada en Java.

Infinispan

Infinispan es un almacén de datos clave/valor distribuido en memoria, con un esquema opcional.

Memcached

Es un sistema de uso general que sirve para administrar la caché distribuida.

Redis

Es un sistema de gestión de bases de datos clave-valor escalable y de alto rendimiento.

ElasticSearch

ElasticSearch es un servidor que utiliza Lucene para la indexación y la búsqueda de datos. Proporciona un motor de búsqueda distribuido y multientidad.

Kafka

Kafka se utiliza para crear pipelines de datos en tiempo real y aplicaciones de streaming.

Swagger

Swagger permite crear documentación interactiva y muy completa de una API REST.

Elastic Stack

Es un sistema construido principalmente alrededor de Elasticsearch, Kibana y Logstash (ELK) con herramientas Beats.

Prometheus

Prometheus es un kit de herramientas de monitorización y alerta de sistemas open source, creado originalmente en SoundCloud.

Thymeleaf

Thymeleaf es un motor de plantillas escrito en Java que puede generar XML/XHTML/HTML5.

Gatling

Gatling es una herramienta de pruebas de escalabilidad.

Cucumber

Cucumber es una herramienta para realizar pruebas automatizadas al estilo de BDD (Behavior-Driven Development).

ArchUnit

ArchUnit es una librería gratuita, sencilla y extensible para verificar la arquitectura de nuestro código Java, utilizando cualquier framework de pruebas unitarias Java.

Testcontainers

Testcontainers es una librería Java que admite pruebas JUnit, proporcionando instancias ligeras y desechables de bases de datos comunes.

3. Lado de la implementación

Lado de la implementación

Funcionalidades

Docker

Docker es una herramienta que puede empaquetar una aplicación y sus dependencias en un contenedor aislado. Se puede ejecutar en cualquier servidor.

Kubernetes

Kubernetes (K8s) es un sistema que permite automatizar el despliegue, el escalado y la administración de aplicaciones en contenedores.

Heroku

Heroku es un servicio de cloud computing de tipo plataforma como servicio (PaaS).

Cloud Foundry

Cloud Foundry es una PaaS open source que permite crear, implementar, ejecutar y escalar aplicaciones en modelos de cloud.

AWS

Amazon Web Services proporciona servicios en el cloud computing fiables, escalables y rentables. La inscripción es gratuita y el pago se realiza por uso.

Boxfuse

Boxfuse permite ejecutar aplicaciones JVM, Node.js y Go en AWS (rápidamente de pago).

Google Cloud Platform

GCP es una plataforma de cloud computing proporcionada por Google, que ofrece alojamiento en la misma infraestructura utilizada por Google internamente.

OpenShift

OpenShift es un servicio de plataforma como servicio de la empresa Red Hat.

Azure Spring Cloud

Incorpora modelos de microservicios modernos a las aplicaciones Spring Boot. Permite desplegar, explotar y escalar de forma fácil aplicaciones en un entorno totalmente administrado.

4. Lado de Spring

JHipster genera una aplicación Spring Boot totalmente configurada respecto a las opciones elegidas durante la creación del proyecto.

Estructura del proyecto

Vamos a ilustrar la arquitectura de las aplicaciones construidas con JHipster a través del estudio de un caso de generación de aplicación Angular/Spring Boot sencilla.

1. La parte front

La parte front puede utilizar Angular, por ejemplo. Es «responsive», es decir, la pantalla se reorganiza si cambiamos el tamaño del área de visualización del navegador web. Se trata de una Single Page Application, es decir, solo hay que actualizar una única página usando AJAX. La aplicación construida utiliza HTML5 Boilerplate (https://html5boilerplate.com/) y Twitter Bootstrap (http://getbootstrap.com). Se generan pruebas para Karma. Angular

Para el desarrollo de la parte front, JHipster utiliza Yeoman con Angular, React o Vue.

En realidad, este generador utiliza todos los mejores frameworks de cliente, que se usan en la actualidad.

2. Las líneas principales de la parte back

Aquí solo presentamos las líneas principales de la parte back.

Para este ejemplo, vamos a utilizar una aplicación monolítica con una base de datos SQL H2 y un front Angular.

La parte front Angular no se presenta porque está fuera del marco de este libro. El lector se podrá apoyar en el libro Angular - Desarrolle sus aplicaciones web con el framework JavaScript de Google de Sébastien OLLIVIER, Daniel DJORDJEVIC y William KLEIN, publicado por Ediciones ENI. Angular

La parte back utiliza Maven, Spring, Spring MVC REST y Spring Data JPA. Se centra en el uso de Spring.

a. Spring Boot

El generador JHipster aprovecha la posibilidad de generar directamente una aplicación Spring Boot preconfigurada. Es muy fácil cambiar la configuración para estar en un stack Spring clásico en caso de que no pueda usar Spring Boot. Para hacer esto, la forma más sencilla es mirar el pom.xml efectivo generado por su herramienta de desarrollo favorita y rehacer el starter.

Spring Boot permite crear rápidamente una aplicación funcional. La configuración se simplifica.

Sistema de gestión de dependencias

Es posible generar los archivos de configuración de la build para Maven y Gradle. Solo veremos los aspectos de generación de proyectos basados en Maven. JHipster utiliza perfiles para personalizar los elementos externos, como la base de datos, para que tengan valores específicos en desarrollo y producción.

Uso de Spring Security

La integración de Spring Security está muy bien hecha, lo que nos permitirá estudiar ejemplos concretos.

Spring MVC REST + Jackson

Aprovecharemos este uso para introducir este tema, que no habíamos tratado antes. Usaremos swagger para autodocumentar los servicios REST directamente desde el navegador.

Soporte opcional de WebSocket con Spring WebSocket

No estudiaremos esta posibilidad porque se sale del marco de este libro.

Spring Data JPA + Bean Validation JPA Bean Validation

Los ejemplos ilustrarán el uso de esta parte de Spring. Más adelante en este capítulo, veremos fragmentos de código que ilustran la integración en Spring MVC.

Evolución de las bases de datos con Liquibase

Hemos estudiado el uso de SQL integrado para las pruebas unitarias. Liquibase es una alternativa interesante desde el punto de vista de los ciclos de evolución del esquema de la base de datos.

Esta herramienta permite ver las modificaciones añadidas al esquema de una base de datos SQL.

Soporte para Elasticsearch

No estudiaremos esta posibilidad porque se sale del marco de este libro.

Bases de datos SQL y NoSQL

JHipster soporta bases de datos SQL y noSQL. Por lo tanto, es posible utilizar Cassandra y MongoDB para bases de datos noSQL.

Nos limitaremos a las bases de datos SQL.

Packaging multiplataforma

Con el mismo packaging, es posible hacer funcionar la aplicación tanto en un entorno de desarrollo como en un uno de producción apuntando a diferentes tipos de bases de datos, típicamente H2 en desarrollo y MySQL u Oracle en producción.

Monitoring y métricas

JHipster integra herramientas de monitoring en las aplicaciones generadas.

JHipster utiliza el sistema Logback que abordaremos en el capítulo sobre elementos externos a Spring.

Otros beneficios no abordados

Podemos usar una caché local EhCache o distribuida con Hazelcast, así como clústeres de servidores de aplicaciones ligeros, pero no detallaremos este punto porque no forma parte del alcance de este libro.

JHipster también permite optimizar recursos estáticos (gzip filters, HTTP cache headers). Utiliza HikariCP para la gestión del grupo de conexiones a fin de optimizar el rendimiento.

Spring Boot

Las principales ventajas de Spring Boot utilizadas aquí son:

  • la creación de aplicaciones Spring autónomas,

  • el servidor HTTP/servlet integrado, que elimina la necesidad de un servidor Jetty o Tomcat externo,

  • el archivo pon.xml simplificado,

  • la configuración automática de Spring en la medida de lo posible,

  • las aplicaciones que se pueden desplegar directamente en producción, con métricas y configuración externalizada.

Dependencias Maven

La aplicación utiliza las siguientes dependencias Maven:

Librería

Versión

maven

3.8.5

spring-boot

2.6.6.RELEASE

hibernate

5.6.7.Final

liquibase

4.6.1

liquibase-hibernate5

4.6.1

h2

1.4.200

validation-api

2.0.1.Final

jaxb-runtime

2.3.3

archunit-junit5

0.22.0

mapstruct

1.4.2.Final

maven-clean-plugin

3.1.0

maven-compiler-plugin

3.11.1

maven-javadoc-plugin

3.3.2

maven-eclipse-plugin

2.10

maven-enforcer-plugin

3.0.0-M3

maven-failsafe-plugin

3.0.0-M5

maven-idea-plugin

2.2.1

maven-resources-plugin

3.2.0

maven-surefire-plugin

3.0.0-M5

maven-war-plugin

3.3.2

maven-checkstyle

3.1.2

checkstyle

10.1

spring-nohttp-checkstyle

0.0.10

git-commit-id-plugin

5.0.0

jacoco-maven-plugin

0.8.7

jib-maven-plugin

3.2.1

lifecycle-mapping

1.0.0

properties-maven-plugin

1.0.0

sonar-maven-plugin

3.9.1.2184

Configuración de la aplicación

La aplicación se basa en una configuración totalmente en Java.

Toda la configuración se encuentra en el paquete com.mycompany.myapp.config y sus subpaquetes.

Clase

Utilidad, anotaciones

Applicationproperties

Propiedades de la aplicación: @ConfigurationProperties

Asyncconfiguration

Configuración asíncrona: @Configuration, @EnableAsync @EnableScheduling

Cacheconfiguration

Administración de caché: @EnableCaching

Clouddatabaseconfiguration

Conceptos básicos del cloud (@Profile(JHipsterConstants.SPRING_PROFILE_CLOUD))

Constants

Las constantes de la aplicación

Databaseconfiguration

Configuración de la base de datos: @EnableJpaRepositories, @EnableJpaAuditing @EnableTransactionManagement

Datetimeformatconfiguration

Configuración de fechas

Feignconfiguration

Configuración de Feign: @EnableFeignClients

Jacksonconfiguration

Configuración de Jackson

Liquibaseconfiguration

Configuración de Liquibase

Localeconfiguration

Gestión lingüística

Loggingaspectconfiguration

Logging de aspectos: @EnableAspectJAutoProxy

Loggingconfiguration

Configuración de los logs: uso de una @RefreshScope (Spring cloud)

Securityconfiguration

Configuración de la seguridad: @EnableWebSecurity, @EnableGlobalMethodSecurity

Webconfigurer

Configuración ServletContextInitializer

La clase Application que contiene el main

Con la anotación @ComponentScan, indicamos que queremos que Spring encuentre los beans.

Dado que la clase está en el paquete raíz de la aplicación, Spring escaneará todos los paquetes, excepto las clases MetricFilterAutoConfiguration y MetricRepositoryAutoConfiguration, mediante el parámetro exclude que especifica qué clases excluir.

@SpringBootApplication 
                  @EnableConfigurationProperties({LiquibaseProperties.class, 
                  ApplicationProperties.class}) 
                    public class Application { 
                [...] 
                } 

El lanzamiento se lleva a cabo en dos etapas. main invoca a la aplicación y @PostConstruct, con el método public void initApplication(), inicializa la aplicación en función de los perfiles activos:

  • Default

  • Prod

  • Fast

  • Cloud

Se lanza el sistema basado en Liquibase y se pone a disposición la base de datos. 

b. La clase de servidor HTTP/servlet

La clase WebConfigure contiene el starter Spring Boot de la aplicación.

El archivo fuente de la clase de configuración es relativamente largo. Lo hemos incluido en su totalidad porque está muy bien autodocumentado.

Clase WebConfigure.java:

@Configuration 
                public class WebConfigurer implements ServletContextInitializerEmbeddedServletContainerCustomizer { 
                  private final Logger log = 
                LoggerFactory.getLogger(WebConfigurer.class); 
                  private final Environment env; 
                  private final JHipsterProperties jHipsterProperties; 
                  private MetricRegistry metricRegistry; 
                  public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) { 
                    this.env = env; 
                    this.jHipsterProperties = jHipsterProperties; 
                  } 
                  @Override 
                  public void onStartup(ServletContext servletContext) throws 
                ServletException { 
                    if (env.getActiveProfiles().length != 0) { 
                      log.info("Web application configuration, using profiles: {}", 
                (Object[]) env.getActiveProfiles()); 
                    } 
                    EnumSet<DispatcherType> disps = EnumSet.of(DispatcherType.REQUEST, 
                DispatcherType.FORWARD, DispatcherType.ASYNC); 
                    initMetrics(servletContext, disps); 
                    if (env.acceptsProfiles(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { 
                      initCachingHttpHeadersFilter(servletContext, disps); 
                    } 
                    if (env.acceptsProfiles(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT)) { 
                      initH2Console(servletContext); 
                    } 
                    log.info("Web application fully configured"); 
                  } 
                 
                  /** 
                   * Customize the Servlet engine: Mime types, the document root, the cache. 
                   */ 
                  @Override 
                  public void customize(ConfigurableEmbeddedServletContainer container) { 
                    MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT); 
                    // IE issue, see https://github.com/jhipster/generator-jhipster/pull/711 
                    mappings.add("html", MediaType.TEXT_HTML_VALUE + ";charset=utf-8"); 
                    // CloudFoundry issue, see https://github.com/cloudfoundry/gorouter/ 
                issues/64 
                    mappings.add("json", MediaType.TEXT_HTML_VALUE + ";charset=utf-8"); 
                    container.setMimeMappings(mappings); 
                    // When running in an IDE or with ./mvnw spring-boot:run, set location 
                of the static web assets. 
                    setLocationForStaticAssets(container); 
                    /* 
                     * Enable HTTP/2 for Undertow - https://twitter.com/ankinson/status/ 
                829256167700492288 
                     * HTTP/2 requires HTTPS, so HTTP requests will fallback to HTTP/1.1. 
                     * See the JHipsterProperties class and your application-*.yml 
                configuration files 
                     * for more information. 
                     */ 
                    if (jHipsterProperties.getHttp().getVersion() 
                .equals(JHipsterProperties.Http.Version.V_2_0) && 
                          container instanceof UndertowEmbeddedServletContainerFactory) { 
                 
                      ((UndertowEmbeddedServletContainerFactory) container) 
                            .addBuilderCustomizers(builder -> 
                                  builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true)); 
                    } 
                  } 
                 
                  private void setLocationForStaticAssets(ConfigurableEmbeddedServlet 
                Container container) { 
                    File root; 
                    String prefixPath = resolvePathPrefix(); 
                    root = new File(prefixPath + "target/www/"); 
                    if (root.exists() && root.isDirectory()) { 
                      container.setDocumentRoot(root); 
                    } 
                  } 
                 
                  /** 
                   * Resolve path prefix to static resources. 
                   */ 
                  private String resolvePathPrefix() { 
                    String fullExecutablePath = this.getClass().getResource("").getPath(); 
                    String rootPath = Paths.get(".").toUri().normalize().getPath(); 
                    String extractedPath = fullExecutablePath.replace(rootPath, ""); 
                    int extractionEndIndex = extractedPath.indexOf("target/"); 
                    if (extractionEndIndex <= 0) { 
                      return ""; 
                    } 
                    return extractedPath.substring(0, extractionEndIndex); 
                  } 
                 
                  /** 
                   * Initializes the caching HTTP Headers Filter. 
                   */ 
                  private void initCachingHttpHeadersFilter(ServletContext servletContext, 
                                                          EnumSet<DispatcherType> disps) { 
                    log.debug("Registering Caching HTTP Headers Filter"); 
                    FilterRegistration.Dynamic cachingHttpHeadersFilter = 
                          servletContext.addFilter("cachingHttpHeadersFilter", 
                                new CachingHttpHeadersFilter(jHipsterProperties)); 
                 
                    cachingHttpHeadersFilter.addMappingForUrlPatterns(disps, true, "/ 
                content/*"); 
                    cachingHttpHeadersFilter.addMappingForUrlPatterns(disps, true, "/app/*"); 
                    cachingHttpHeadersFilter.setAsyncSupported(true); 
                  } 
                 
                  /** 
                   * Initializes Metrics. 
                   */ 
                  private void initMetrics(ServletContext servletContext, 
                EnumSet<DispatcherType> disps) { 
                    log.debug("Initializing Metrics registries"); 
                    servletContext.setAttribute(InstrumentedFilter.REGISTRY_ATTRIBUTE, 
                          metricRegistry); 
                    servletContext.setAttribute(MetricsServlet.METRICS_REGISTRY, 
                          metricRegistry); 
                 
                    log.debug("Registering Metrics Filter"); 
                    FilterRegistration.Dynamic metricsFilter = 
                servletContext.addFilter("webappMetricsFilter", 
                          new InstrumentedFilter()); 
                 
                    metricsFilter.addMappingForUrlPatterns(disps, true, "/*"); 
                    metricsFilter.setAsyncSupported(true); 
                 
                    log.debug("Registering Metrics Servlet"); 
                    ServletRegistration.Dynamic metricsAdminServlet = 
                          servletContext.addServlet("metricsServlet", new MetricsServlet()); 
                 
                    metricsAdminServlet.addMapping("/management/metrics/*"); 
                    metricsAdminServlet.setAsyncSupported(true); 
                    metricsAdminServlet.setLoadOnStartup(2); 
                  } 
                 
                  @Bean 
                  public CorsFilter corsFilter() { 
                    UrlBasedCorsConfigurationSource source = new 
                UrlBasedCorsConfigurationSource(); 
                    CorsConfiguration config = jHipsterProperties.getCors(); 
                    if (config.getAllowedOrigins() != null && 
                !config.getAllowedOrigins().isEmpty()) { 
                      log.debug("Registering CORS filter"); 
                      source.registerCorsConfiguration("/api/**", config); 
                      source.registerCorsConfiguration("/management/**", config); 
                      source.registerCorsConfiguration("/v2/api-docs", config); 
                    } 
                    return new CorsFilter(source); 
                  } 
                 
                  /** 
                   * Initializes H2 console. 
                   */ 
                  private void initH2Console(ServletContext servletContext) { 
                    log.debug("Initialize H2 console"); 
                    try { 
                      // We don't want to include H2 when we are packaging for the "prod" 
                profile and won't 
                      // actually need it, so we have to load / invoke things at runtime 
                through reflection. 
                      ClassLoader loader = Thread.currentThread().getContextClassLoader(); 
                      Class<?> servletClass = Class.forName("org.h2.server.web.WebServlet", 
                true, loader); 
                      Servlet servlet = (Servlet) servletClass.newInstance(); 
                 
                      ServletRegistration.Dynamic h2ConsoleServlet = 
                servletContext.addServlet("H2Console", servlet); 
                      h2ConsoleServlet.addMapping("/h2-console/*"); 
                      h2ConsoleServlet.setInitParameter("-properties", "src/main/resources/"); 
                      h2ConsoleServlet.setLoadOnStartup(1); 
                 
                    } catch (ClassNotFoundException | LinkageError e) { 
                      throw new RuntimeException("Failed to load and initialize 
                org.h2.server.web.WebServlet", e); 
                 
                    } catch (IllegalAccessException | InstantiationException e) { 
                      throw new RuntimeException("Failed to instantiate 
                org.h2.server.web.WebServlet", e); 
                    } 
                  } 
                 
                  @Autowired(required = false) 
                  public void setMetricRegistry(MetricRegistry metricRegistry) { 
                    this.metricRegistry = metricRegistry; 
                  } 
                } 

La clase WebConfigure implementa la interfaz ServletContextInitializer de Spring Boot y la interfaz EmbeddedServletContainerCustomizer.

El método onStartup tiene en cuenta el entorno para inicializar partes específicas de la aplicación bajo demanda, en función de los entornos:

@Override 
                public void onStartup(ServletContext servletContext) throws ServletException { 
                  if (env.getActiveProfiles().length != 0) { 
                    log.info("Web application configuration, using profiles: {}", (Object[]) 
                env.getActiveProfiles()); 
                  } 
                  if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_ 
                DEVELOPMENT))) { 
                    initH2Console(servletContext); 
                  } 
                  log.info("Web application fully configured"); 
                } 
  • initMetrics

SPRING_PROFILE_PRODUCTION

  • initCachingHttpHeadersFilter

SPRING_PROFILE_DEVELOPMENT

  • initH2Consola

A continuación, la aplicación inicializa la consola H2 para los perfiles que la utilizan.

Utilizamos Spring Security para la autenticación y los permisos.

Esta clase de inicialización es un buen modelo de referencia. Permite, en pocas líneas, definir un servidor HTTP/servlet, configurar módulos en función de los entornos, añadir filtros de servlet para comprimir flujos en formato GZip, etc.

  • La clase de configuración de la seguridad de las URL:

La clase SecurityConfiguration proporciona configuración de seguridad.

Clase SecurityConfiguration.java:

@EnableWebSecurity 
                @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) 
                @Import(SecurityProblemSupport.class) 
                public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 
                 
                  private final TokenProvider tokenProvider; 
                  private final SecurityProblemSupport problemSupport; 
                 
                  public SecurityConfiguration(TokenProvider tokenProvider, 
                SecurityProblemSupport problemSupport) { 
                    this.tokenProvider = tokenProvider; 
                    this.problemSupport = problemSupport; 
                  } 
                  @Override 
                  public void configure(WebSecurity web) { 
                    web.ignoring() 
                      .antMatchers("/h2-console/**"); 
                  } 
                 
                  @Override 
                  public void configure(HttpSecurity http) throws Exception { 
                    // @formatter:off 
                    http 
                      .csrf() 
                      .disable() 
                      .exceptionHandling() 
                        .authenticationEntryPoint(problemSupport) 
                        .accessDeniedHandler(problemSupport) 
                    .and() 
                      .headers() 
                      .contentSecurityPolicy("default-src 'self'; frame-src 'self' data:; 
                script-src 'self' 'unsafe-inline' 'unsafe-eval' 
                https://storage.googleapis.com; style-src 'self' 'unsafe-inline';  
                img-src 'self' data:; font-src 'self' data:") 
                    .and() 
                .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy. 
                STRICT_ORIGIN_WHEN_CROSSORIGIN) 
                    .and() 
                      .featurePolicy("geolocation 'none'; midi 'none'; sync-xhr 'none'; 
                microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; 
                speaker 'none'; fullscreen 'self'; payment 'none'") 
                    .and() 
                      .frameOptions() 
                      .deny() 
                    .and() 
                      .sessionManagement() 
                      .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 
                    .and() 
                      .authorizeRequests() 
                      .antMatchers("/api/authenticate").permitAll() 
                      .antMatchers("/api/**").authenticated() 
                      .antMatchers("/management/health").permitAll() 
                      .antMatchers("/management/info").permitAll() 
                      .antMatchers("/management/prometheus").permitAll() 
                      .antMatchers("/management/ 
                **").hasAuthority(AuthoritiesConstants.ADMIN) 
                    .and() 
                      .apply(securityConfigurerAdapter()); 
                    // @formatter:on 
                  } 
                 
                  private JWTConfigurer securityConfigurerAdapter() { 
                    return new JWTConfigurer(tokenProvider); 
                  } 
                } 

Las anotaciones indican que tenemos una clase de configuración.

Esta clase también especifica cómo cifrar las contraseñas.

Prepara y configura el servicio de los usuarios (userDetailsService).

A continuación, filtra las URL que no estarán sujetas a seguridad.

El método protected void configure(HttpSecurity http) concentra la configuración:

  • La gestión del login y del logout,

  • la gestión del «remember me» para guardar los argumentos de conexión entre dos sesiones,

  • la seguridad de las URL.

La seguridad de las URL es muy simple de implementar.

  • La clase de configuración de seguridad SecurityUtils.

La clase SecurityUtils permite recuperar a la persona conectada, saber si está autenticada y si asume un rol pasado como argumento.

Este es el código de la clase SecurityUtils:

public final class SecurityUtils { 
                 
                  private SecurityUtils() { 
                  } 
                 
                  /** 
                   * Get the login of the current user. 
                   * 
                   * @return the login of the current user. 
                   */ 
                  public static Optional<String> getCurrentUserLogin() { 
                    SecurityContext securityContext = SecurityContextHolder.getContext(); 
                    return 
                Optional.ofNullable(extractPrincipal(securityContext.getAuthentication())); 
                  } 
                 
                  private static String extractPrincipal(Authentication authentication) { 
                    if (authentication == null) { 
                      return null; 
                    } else if (authentication.getPrincipal() instanceof UserDetails) { 
                      UserDetails springSecurityUser = (UserDetails) 
                authentication.getPrincipal(); 
                      return springSecurityUser.getUsername(); 
                    } else if (authentication.getPrincipal() instanceof String) { 
                      return (String) authentication.getPrincipal(); 
                    } 
                    return null; 
                  } 
                 
                 
                  /** 
                   * Get the JWT of the current user. 
                   * 
                   * @return the JWT of the current user. 
                   */ 
                  public static Optional<String> getCurrentUserJWT() { 
                    SecurityContext securityContext = SecurityContextHolder.getContext(); 
                    return Optional.ofNullable(securityContext.getAuthentication()) 
                      .filter(authentication -> authentication.getCredentials() instanceof 
                String) 
                      .map(authentication -> (String) authentication.getCredentials()); 
                  } 
                 
                  /** 
                   * Check if a user is authenticated. 
                   * 
                   * @return true if the user is authenticated, false otherwise. 
                   */ 
                  public static boolean isAuthenticated() { 
                    Authentication authentication =  
                SecurityContextHolder.getContext().getAuthentication(); 
                    return authentication != null && 
                    getAuthorities(authentication).noneMatch(AuthoritiesConstants. 
                ANONYMOUS::equals); 
                  } 
                 
                  /** 
                   * If the current user has a specific authority (security role). 
                   * <p> 
                   * The name of this method comes from the {@code isUserInRole()} method  
                in the Servlet API. 
                   * 
                   * @param authority the authority to check. 
                   * @return true if the current user has the authority, false otherwise. 
                   */ 
                  public static boolean isCurrentUserInRole(String authority) { 
                    Authentication authentication =  
                SecurityContextHolder.getContext().getAuthentication(); 
                    return authentication != null && 
                      getAuthorities(authentication).anyMatch(authority::equals); 
                  } 
                 
                  private static Stream<String> getAuthorities(Authentication authentication) { 
                    return authentication.getAuthorities().stream() 
                      .map(GrantedAuthority::getAuthority); 
                  } 
                } 

Los roles se identifican en la clase AuthoritiesConstants.

Lista de roles en la aplicación:

Rol

Constante asociada

ADMIN

ROLE_ADMIN

USER

ROLE_USER

ANONYMOUS

ROLE_ANONYMOUS

  • Spring Data JPA + Bean Validation

La capa de dominio se ha mejorado para que los servicios REST puedan utilizarla.

En esta capa especificaremos los campos que se ignorarán durante los intercambios JSON.

  • la clase Operation

JHipster respeta el modelo en capas.

Ya hemos presentado las capas del dominio.

Las anotaciones en la clase indican que tenemos una entidad JPA: JPA

@Entity 
                @Table(name = "operation"@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 

Algunas anotaciones son de Hibernate porque las funcionalidades relacionadas no están disponibles con JPA:

import org.hibernate.annotations.Cacheimport org.hibernate.annotations.CacheConcurrencyStrategy; 

JHipster hace un buen uso de los nuevos formatos de fecha Java 8.

  @NotNull 
                  @Column(name = "jhi_date", nullable = false) 
                  private Instant date; 

Spring MVC REST + Jackson

Las librerías Spring MVC REST con el soporte de Jackson permiten que la capa Repository exponga la capa de dominio de manera muy simple.

Es suficiente con declarar las interfaces y Spring deduce las implementaciones.

Siempre podemos agregar nuestras propias implementaciones para repositorios complejos.

La clase Repository

La clase Repository contiene el repositorio JPA para la entidad.

Vemos que está vacío porque Spring proporciona directamente todas las API que usamos más adelante en el código. Si queremos tener métodos adicionales, es suficiente con declararlos para que Spring Data añada el código por nosotros. 

Clase Repository:

@SuppressWarnings("unused") 
                @Repository 
                public interface EntreeRepository extends JpaRepository<Entree, Long> { 
                 
                    @Query(value = "select distinct entree from Entree entree left join  
                fetch entree.etiquettes", 
                        countQuery = "select count(distinct entree) from Entree entree") 
                    Page<Entree> findAllWithEagerRelationships(Pageable pageable); 
                 
                    @Query("select distinct entree from Entree entree left join fetch  
                entree.etiquettes") 
                    List<Entree> findAllWithEagerRelationships(); 
                 
                    @Query("select entree from Entree entree left join fetch  
                entree.etiquettes where entree.id =:id") 
                    Optional<Entree> findOneWithEagerRelationships(@Param("id") Long id); 
                } 

Se trata de un caso muy sencillo.

La clase UserRepository contiene el repositorio JPA para la entidad User.

Spring proporciona directamente todas las API que usamos con posterioridad en el código y solicitamos de forma explícita API adicionales añadiendo solo las firmas de los métodos.

Clase UserRepository:

@Repository 
                public interface UserRepository extends JpaRepository<User, Long> { 
                  String USERS_BY_LOGIN_CACHE = "usersByLogin"; 
                  String USERS_BY_EMAIL_CACHE = "usersByEmail"; 
                  Optional<User> findOneByActivationKey(String activationKey); 
                  List<User> findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreated 
                DateBefore(Instant dateTime); 
                  Optional<User> findOneByResetKey(String resetKey); 
                  Optional<User> findOneByEmailIgnoreCase(String email); 
                  Optional<User> findOneByLogin(String login); 
                  @EntityGraph(attributePaths = "authorities") 
                  @Cacheable(cacheNames = USERS_BY_LOGIN_CACHE) 
                  Optional<User> findOneWithAuthoritiesByLogin(String login); 
                  @EntityGraph(attributePaths = "authorities") 
                  @Cacheable(cacheNames = USERS_BY_EMAIL_CACHE) 
                  Optional<User> findOneWithAuthoritiesByEmailIgnoreCase(String email); 
                  Page<User> findAllByLoginNot(Pageable pageable, String login); 
                } 

La capa Service

Esta capa solo contiene procesos de negocio complejos. En general, un proceso se considerará complejo si involucra varios objetos de la capa modelo o si hay acciones particulares para realizar que van más allá del marco estricto de la capa de repository.

En JHipster, en esta capa solo encontramos clases relativas a la gestión del usuario, correos electrónicos y eventos de auditoría.

La capa Resource

Esta capa contiene los controladores Spring MVC. Los métodos trabajan en general directamente con la capa repository y con la capa intermediaria service para entidades complejas. Considere la clase OperationResource.

La mayor parte de los procesamientos se concentra en esta clase para exponer los datos a través de servicios REST, para la explotación de estos datos por parte de Angular.

Ya hemos visto la clase de recurso web generada.

La anotación @RestController indica que tenemos un controlador REST. Vemos en esta clase cómo se realizan las diferentes operaciones, como la creación/modificación/eliminación.

Se monitorizan los métodos anotados con @Times.

JHipster y WebFlux

WebFlux se ha integrado en JHipster 6 y mejorado en JHipster 7.

Esta adición era muy esperada por los usuarios. Ya es muy completo y permite la experimentación.

A nivel de base de datos, ofrece:

  • SQL (H2, MySQL, PostgreSQL, MSSQL)

  • MongoDB

  • Cassandra

  • Couchbase

  • [BETA] Neo4j

  • No database

Si elegimos SQL, entonces propone usar R2DBC:

  • MySQL

  • PostgreSQL

  • Microsoft SQL Server

Las capas domain, service y web se ajustan a las especificaciones que vimos en el capítulo sobre WebFlux.

Tener una aplicación responsiva cambia un poco algunas clases de configuración.

1. Configuración DatabaseConfiguration

Para las bases de datos SQL, JHipster utiliza R2DBC con la anotación @EnableR2dbcRepositories en la clase DatabaseConfiguration.

Disponemos de convertidores para las fechas:

@Bean 
      public R2dbcCustomConversions r2dbcCustomConversions(R2dbcDialect 
      dialect) { 
       List<Object> converters = new ArrayList<>(); 
       converters.add(InstantWriteConverter.INSTANCE); 
       converters.add(InstantReadConverter.INSTANCE); 
       converters.add(BitSetReadConverter.INSTANCE); 
       converters.add(DurationWriteConverter.INSTANCE); 
       converters.add(DurationReadConverter.INSTANCE); 
       converters.add(ZonedDateTimeReadConverter.INSTANCE); 
       converters.add(ZonedDateTimeWriteConverter.INSTANCE); 
       return R2dbcCustomConversions.of(dialect, converters); 
      } 
      [...] 
      @WritingConverter 
      public enum InstantWriteConverter implements Converter<Instant,  
      LocalDateTime> { 
       INSTANCE; 
       
       public LocalDateTime convert(Instant source) { 
        return LocalDateTime.ofInstant(source, ZoneOffset.UTC); 
       } 
      } 
       
      @ReadingConverter 
      public enum InstantReadConverter implements Converter<LocalDateTime, 
      Instant> { 
       INSTANCE; 
       
       @Override 
       public Instant convert(LocalDateTime localDateTime) { 
        return localDateTime.toInstant(ZoneOffset.UTC); 
       } 
      } 
       
       @ReadingConverter 
       public enum BitSetReadConverter implements 
      Converter<BitSet, Boolean> { 
       INSTANCE; 
       
       @Override 
       public Boolean convert(BitSet bitSet) { 
        return bitSet.get(0); 
       } 
      } 
       
       @ReadingConverter 
       public enum ZonedDateTimeReadConverter implements 
      Converter<LocalDateTime, ZonedDateTime> { 
       INSTANCE; 
       
       @Override 
       public ZonedDateTime convert(LocalDateTime localDateTime) { 
        // Be aware - we are using the UTC timezone 
        return ZonedDateTime.of(localDateTime, ZoneOffset.UTC); 
       } 
      } 
       
       @WritingConverter 
       public enum ZonedDateTimeWriteConverter implements 
      Converter<ZonedDateTime, LocalDateTime> { 
       INSTANCE; 
       
       @Override 
       public LocalDateTime convert(ZonedDateTime zonedDateTime) { 
        return zonedDateTime.toLocalDateTime(); 
       } 
      } 
       
       @WritingConverter 
       public enum DurationWriteConverter implements Converter<Duration, 
      Long> { 
       INSTANCE; 
       
       @Override 
       public Long convert(Duration source) { 
        return source != null ? source.toMillis() : null; 
       } 
      } 
       
       @ReadingConverter 
       public enum DurationReadConverter implements Converter<Long, 
      Duration> { 
       INSTANCE; 
       
       @Override 
       public Duration convert(Long source) { 
        return source != null ? Duration.ofMillis(source) : null; 
        } 
      } 

2. Configuración DateTimeFormatConfiguration

Implementamos WebFluxConfigure en lugar de WebMvcConfigure.

3. Configuración LocaleConfiguration

También importamos @Import(WebFluxAutoConfiguration.class).

Corresponde a las anotaciones:

@Configuration(proxyBeanMethods=false) 
      @ConditionalOnWebApplication(type=REACTIVE) 
      @ConditionalOnClass(value=org.springframework.web.reactive. 
      config.WebFluxConfigurer.class@ConditionalOnMissingBean(value=org.springframework.web.reactive 
      .config.WebFluxConfigurationSupport.class@AutoConfigureAfter(value={ReactiveWebServerFactoryAutoConfiguration 
      .class,CodecsAut 
      Configuration.class,ValidationAutoConfiguration.class}) 
      @AutoConfigureOrder(value=-2147483638) 
      public class WebFluxAutoConfiguration 

4. Configuración ReactorConfiguration

Tenemos un hack que nos permite depurar los operadores.

@Configuration 
      @Profile("!" + JhipsterConstants.SPRING_PROFILE_PRODUCTION) 
      public class ReactorConfiguration { 
          public ReactorConfiguration() { 
              Hooks.onOperatorDebug(); 
          } 
      } 

5. Configuración SecurityConfiguration

En lugar de:

@EnableWebSecurity 
      @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) 

Tenemos el equivalente:

@EnableWebFluxSecurity 
      @EnableReactiveMethodSecurity 

6. Configuración WebConfigurer

Implementamos WebFluxConfigure en lugar de ServletContext-Initializer, como para DateTimeFormatConfiguration.

7. Las pruebas

Las pruebas se migran parcialmente.

Usan un truco para continuar haciendo procedimientos con API receptivas, utilizando el método bloc() para Mono y blockFirst() y blockLast() para Flux.

Estos métodos se deben evitar en el código de la aplicación, ya que pueden bloquear el pipeline reactivo bloqueando el bucle de eventos.

VM6883:64

Puntos clave

  • JHipster genera una aplicación Spring limpia, que puede servir como modelo, y ofrece un buen soporte para hipermedia y HATEOAS.

  • JHipster permite probar y comparar diferentes opciones de generaciones.

  • JHipster es muy bueno para descubrir las tecnologías del momento.

  • Podemos personalizar fácilmente el código generado por JHipster.

VM6883:64

Introducción

GraphQL es una alternativa a las API REST. Consiste en un lenguaje de consulta y un entorno de ejecución. Fue creado por Facebook en 2012 y luego abrió el código en 2015. GraphQL

Como recordatorio, aquí están las evoluciones de las formas de llamar a un proceso remoto:

Año

Protocolo

Explicación

1980

PRC

Remote Procedure Call: llamada al sistema externo con IDL (Interface Definition Language).

1998

XML-RPC

Uso de XML para intercambios.

2005

JSON-RPC

Uso de JSON para intercambios.

19982009

SOAP (sucesor de XML-RPC)

Uso de XML para intercambios con un IDL: WSDL.

2000

REST con verbos HTTP

El servidor elige la representación de los datos en la respuesta, REST sin IDL: Swagger y luego OpenAPI.

2007

FQL

Facebook Query Language: GET /fql?q=<consulta SELECT...>.

2008

YQL

Yahoo Query Language: GET /fql?q= <consulta SELECT… con joins >, API de composición.

2012

GraphQL

Sigue el protocolo RPC más universal: con fetch/mutación/suscription.

GraphQL se basa en consultas POST, que, por lo tanto, se deben almacenar en caché en el lado del cliente. Estas consultas indican el orden y la selección de los campos que se devolverán desde el servidor. El servidor define el contrato de intercambio a través de un esquema. Este contrato especifica cómo leer y escribir datos. GraphQL intercambia los datos a través de un grafo y se puede utilizar con bases de datos SQL, NoSQL, grafo, etc. Está fuertemente tipado y el contenido de los intercambios está predeterminado. A cambio, esto evita tener datos con contenido insuficiente (under-fetching) o sobrevalorados (over-fetching).

GraphQL se encuentra en la capa de enrutamiento de la API.

El grafo de GraphQL se corresponde con la vista del objeto central devuelto por una consulta que, a su vez, se corresponde con la entidad rodeada por las entidades vinculadas. También es posible cargar listas de objetos a la vez o mediante una suscripción a un flujo.

El esquema

Antes de ver las consultas, necesitamos estudiar el esquema de GraphQL en el que se basan, que tiene un formato específico.

El esquema permite listar los datos que los clientes solicitan a través de tipos de objeto. El desarrollador de la API asocia cada campo de un esquema con una función llamada resolver, que genera un valor en tiempo de ejecución.

El esquema se divide en secciones que se identifican por su tipo. Existen los tipos específicos de las operaciones y los relativos a la descripción de los datos.

Un esquema es un grafo que normalmente se diseña y describe en cuatro pasos: 

  • Type Object

  • Type Scalar

  • Type Query

  • Type Mutation

Comencemos con los nodos del grafo que representan los datos:

type Libro { 
      } 
       
      type Autor { 
      } 

A continuación, agreguemos los tipos escalares (Scalar), utilizando los tipos integrados Int, Float, String, Boolean e ID. A nivel de tipo, podemos indicar que la variable es opcional usando «!».

type Libro { 
       titulo: String! 
       Contenido: String! 
      } 
       
      type Autor { 
       nombre: String! 
      } 

A continuación, añadimos las relaciones entre los objetos:

type Libro { 
       titulo: String! 
       Contenido: String! 
       # necesario para los libros que tienen un autor. 
       tienePorAutor: Autor! 
      } 
       
      type Autor { 
       nombre: String! 
       # Un autor puede haber escrito varios libros 
       haEscrito: [Libro] 
      } 

Solo resta escribir operaciones de lectura/escritura en estas entidades.

El esquema describe tres tipos de operaciones:

  • Las consultas

  • Las mutaciones

  • Las suscripciones

1. La sección Query Query

En esta sección se enumeran las posibles consultas en modo solo lectura.

El tipo Query especifica los criterios de consulta para cada búsqueda. Es necesario optimizar las consultas para paliar los problemas de under-fetching y over-fetching. El under-fetching se encuentra cuando tenemos que hacer varias llamadas para tener la integridad de los datos. El over-fetching se produce cuando devolvemos datos que el cliente solo utiliza parcialmente. Por lo tanto, es necesario tener en cuenta las necesidades previas. under-fetching over-fetching

type Query { 
       # Devuelve todos los libros 
       recuperarTodosLosLibros(): [Libros] 
       # Devuelve un libro en función de su ID. 
       recuperarUnLibro(id: ID!): Libro 
      } 
       
      type Query { 
       recuperarTodosLosAutores(): [Autor] 
       recuperarUnAutor(id: ID!): Autor 
      } 

2. La sección Mutation Mutation

En esta sección se enumeran las posibles consultas de escritura.

type Mutation { 
       agregarUnLibro(titulo: String!, contenido: String!, autorID: 
      ID!): Libro 
      } 
       
      type Mutation { 
       addBlogPost(title: String!, content: String!, authorID: 
      ID!): BlogPost! 
      } 

También es posible configurar una suscripción que informe de un flujo asíncrono de respuesta al cliente. Hay un ejemplo en los ejemplos descargables.

Podemos añadir directivas. Estas directivas se configuran para aplicarse solo a determinados lugares de la declaración de esquema.

Directiva

Descripción

@deprecated(reason: String)

Depreciación u obsoletización de un campo.

@skip(if: Boolean!)

Exclusión condicional de un campo o fragmento.

@include(if: Boolean!)

Inclusión condicional de un campo o fragmento.

@specifiedBy

URL que permite describir las particularidades de este campo.

"Directs the executor to include this field or fragment only when the 
      `if` argument is true"  
      directive @include( 
         "Included when true." 
         if: Boolean! 
       ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 
       
      "Directs the executor to skip this field or fragment when the  
      `if`'argument is true."  
      directive @skip( 
         "Skipped when true." 
         if: Boolean! 
       ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 
       
      "Marks the field, argument, input field or enum value as deprecated" 
      directive @deprecated( 
         "The reason for the deprecation" 
         reason: String = "No longer supported" 
       ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | 
      INPUT_FIELD_DEFINITION 
       
      "Exposes a URL that specifies the behaviour of this scalar."  
      directive @specifiedBy( 
         "The URL that specifies the behaviour of this scalar." 
         url: String! 
       ) on SCALAR 

Las directivas se utilizan como las anotaciones:

type EjemploConDepreciation { 
       oldField: String @deprecated(reason: "Use `newField`.") 
       newField: String 
      } 

Estas pautas simplifican la escritura de esquemas y Spring las utiliza.

También podemos especificar rangos de valores a través de enumeraciones:

enum ProjectStatus { 
       ACTIVE 
       DESACTIVE 
      } 
VM6883:64

Integración de GraphQL en Spring

Inicialmente, teníamos el proyecto GraphQL Java Spring, creado por los equipos de GraphQL: https://github.com/graphql-java/graphql-java-spring. Este proyecto evolucionó para integrarse en Spring con el nombre de Spring for GraphQL y ahora sirve como proyecto base para cualquier proyecto nuevo. La reciente integración directa de GraphQL en Spring aún no es GA (General Availability). En el momento de escribir este libro, la versión todavía estaba en 1.0.0-M6 PRE. Su ventaja es que simplifica la exposición de la API a través de una adaptación de los controladores REST clásicos Spring.

Los ejemplos de Spring se pueden encontrar en la dirección: https://github.com/spring-projects/spring-graphql

Como mínimo, GraphQL requiere:

  • JDK8

  • Spring Framework 5.3

  • GraphQL Java 17

  • Spring Data 2021.1.0 o más para el QueryDSL y el Query, por ejemplo

La capa de transporte puede utilizar HTTP y los WebSockets. Del mismo modo, podemos usar Spring MVC o Spring WebFlux.

Podemos usar la extensión Spring Data Querydsl con GraphQL.

VM6883:64

La extensión Spring Data Querydsl Spring Data Querydsl

Querydsl es una librería que permite simplificar la creación de predicados de consultas, generando un metamodelo con ayuda de un preprocesador de anotaciones. La librería es independiente de Spring y está disponible en: http://querydsl.com/

A continuación, se muestra un ejemplo simple para ilustrar su uso:

List<Person> persons = queryFactory.selectFrom(person) 
       .where( 
         person.firstName.eq("John"), 
         person.lastName.eq("Doe")) 
       .fetch(); 

Usarlo con GraphQL y Spring simplifica el código. Creamos un Bean Repository de tipo QuerydslPredicateExecutor que manipula DataFetcher. Spring Data cubre JPA, MongoDB y LDAP para el uso de QuerydslPredicateExecutor. QuerydslPredicateExecutor

Por ejemplo:

Para un resultado único:

// For single result queries 
      DataFetcher<Account> dataFetcher = 
             QuerydslDataFetcher.builder(repository).single(); DataFetcher 

Para obtener un resultado en forma de lista:

// For multi-result queries 
      DataFetcher<Iterable<Account>> dataFetcher = 
             QuerydslDataFetcher.builder(repository).many(); 

DataFetcher construye un Querydsl Predicate a partir de los argumentos de consulta GraphQL. Posteriormente, lo utiliza para procesar el acceso a los datos. Su uso es hasta cierto punto delicado, porque es necesario añadir un preprocesador para las anotaciones en la configuración Maven:

<dependencies> 
         <!-- ... --> 
         <dependency> 
             <groupId>com.querydsl</groupId> 
             <artifactId>querydsl-apt</artifactId> 
             <version>${querydsl.version}</version> 
             <classifier>jpa</classifier> 
             <scope>provided</scope> 
         </dependency> 
         <dependency> 
             <groupId>org.hibernate.javax.persistence</groupId> 
             <artifactId>hibernate-jpa-2.1-api</artifactId> 
             <version>1.0.2.Final</version> 
         </dependency> 
         <dependency> 
             <groupId>javax.annotation</groupId> 
             <artifactId>javax.annotation-api</artifactId> 
             <version>1.3.2</version> 
         </dependency> 
      </dependencies> 
      <plugins> 
         <!-- Annotation processor configuration --> 
         <plugin> 
             <groupId>com.mysema.maven</groupId> 
             <artifactId>apt-maven-plugin</artifactId> 
             <version>${apt-maven-plugin.version}</version> 
             <executions> 
                 <execution> 
                     <goals> 
                         <goal>process</goal> 
                     </goals> 
                     <configuration> 
                         <outputDirectory>target/generated-sources/ 
      java</outputDirectory> 
                         <processor>com.querydsl.apt.jpa. 
      JPAAnnotationProcessor</processor> 
                     </configuration> 
                 </execution> 
             </executions> 
         </plugin> 
      </plugins> 

Spring establece la correspondencia de nombres de variables con nombres de columnas. Es posible personalizar la correspondencia usando un QuerydslBinderCustomizer. Spring Boot guarda automáticamente durante el arranque el Repository anotado por la anotación @GraphQlRepository, a través de RuntimeWiringConfigurer. QuerydslBinderCustomizer RuntimeWiringConfigurer

Controladores GraphQL

Los controladores GraphQL son controladores REST clásicos que se personalizan. AnnotatedControllerConfigurer detecta estos controladores. Especificamos que un método de controlador se corresponde con un campo de consulta usando la anotación @QueryMapping. La consulta se determina a partir del nombre del método si el método no se especifica como argumento de la anotación. AnnotatedControllerConfigurer

@Controller 
      public class HolaController { 
       
             @QueryMapping 
             public String hola() { 
                 return "Hola a ti"; 
             } 
      } 

RuntimeWiring.Builder se utiliza para registrar la consulta llamada «hola» como graphql.schema.DataFetcher.

@SchemaMapping

Es posible personalizar el nombre del tipo padre y el nombre del campo en la anotación @SchemaMapping:

@Controller 
      public class LibroController { 
       
         @SchemaMapping(typeName="Libro", field="autor ") 
         public Autor getAutor(Libro libro) { 
             // ... 
         } 
      } 

La anotación se puede declarar a nivel de la clase, para especificar un nombre de tipo predeterminado para todos los métodos de la clase.

@Controller 
      @SchemaMapping(typeName="Libro"public class LibroController { 
      ... 
      } 

Las Consulta, Mutación y Suscripción se indican usando anotaciones @QueryMapping, @MutationMapping y @SubscriptionMapping que, a su vez, son meta-anotaciones anotadas con @SchemaMapping.

@Controller 
      public class LibroController { 
       
         @QueryMapping 
         public Libro libroParId(@Argument Long id) { 
             // ... 
         } 
       
         @MutationMapping 
         public Book addLibro(@Argument LibroInput libroInput) { 
             // ... 
         } 
       
         @SubscriptionMapping 
         public Flux<Libro> nuevasPublicaciones() { 
             // ... 
         } 
      } 

La firma de los métodos (el tipo devuelto y los argumentos) permite crear consultas automáticamente.

Existe una amplia gama de tipos de respuesta:

Argumento del método

La descripción

@Argument

Para acceder a los argumentos de campo con conversión (consulte @Argument).

@ProjectedPayloadInterface

Para acceder a los argumentos de campo a través de una interfaz de proyecto (consulte @ProjectPayloadInterface).

La source

Para acceder a la instancia source (es decir, padre/contenedor) del campo (consulte Source).

DataLoader

Para acceder a DataLoader en el DataLoaderRegistry.

@ContextValue

Para tener acceso a un valor de localContext, si se trata de una instancia de GraphQLContext, o de GraphQLContext, si es un DataFetchingEnvironment.

GraphQLContext

Para tener acceso al contexto desde DataFetchingEnvironment.

java.security.Principal

Obtenido a partir del contexto Spring Security si está disponible.

DataFetchingFieldSelectionSet

Para tener acceso al conjunto de selección de la consulta a través de DataFetchingEnvironment.

Locale,Optional<Locale>

Para acceder al Locale desde DataFetchingEnvironment.

DataFetchingEnvironment

Para obtener un acceso directo al DataFetchingEnvironment subyacente.

VM6883:64

Autoconfiguración

Solo detallamos la versión HTTP a título ilustrativo. Un ejemplo adicional basado en WebFlux está disponible en los ejemplos descargables. La configuración automática se realiza a través de GraphQlWebMvcAutoConfiguration. Hay un equivalente para WebFlux: GraphQlWebFluxAutoConfiguration. GraphQlWebMvcAutoConfiguration GraphQlWebFluxAutoConfiguration

1. Versión HTTP

De la misma manera que sucede con las aplicaciones RESTful web services, los servicios GraphQL se basan en un Controlador REST que llama a un servicio DAO o a un Repository, que permite acceder a los datos de una base de datos. Hay Beans Spring especializados en simplificar el código.

El handler GraphQlHttpHandler gestiona las consultas GraphQL para las consultas HTTP a través de interceptores de consultas web. Las consultas web pasan por el verbo POST, con el detalle solicitado especificado en el body con un formato JSON que cumple con la especificación GraphQL por HTTP. GraphQlHttpHandler

Hay dos niveles de control. El primer nivel verifica que la consulta HTTP esté bien formada y que el body JSON sea decodificable. En caso de éxito, tenemos el estado de la respuesta HTTP a OK (200). El segundo nivel se posiciona por el solicitante GraphQL, que sitúa los eventuales errores en el campo «errors» de la respuesta. Se utiliza un bean RouterFunction para crear un endpoint que utiliza el handler GraphQlHttpHandler.

2. El Service GraphQlService GraphQlService

La interfaz GraphQlService es la interfaz genérica para procesar las consultas. Su implementación principal es la clase DefaultExecutionGraphQlService que implementa la interfaz ExecutionGraphQlService, que es una facade para usar la clase graphql.GraphQL de la librería original encapsulada por Spring. De forma predeterminada, Spring invoca un DefaultExecutionGraphQlService para los Beans declarados a través de la interfaz GraphQlService.

La instancia del objeto graphql.GraphQL es accesible a través de la abstracción Spring GraphQLSource, que se genera de forma predeterminada por el builder GraphQlSource.builder(). GraphQlSource

Los esquemas

Los esquemas se administran como Recursos, que pueden ser archivos clásicos cargados de forma predeterminada por Spring Boot al inicio si están en el directorio estándar. Podemos usar otros tipos de Resource si es necesario.

GraphQlSource.Builder utiliza el GraphQL GraphQLSchemaGenerator de forma predeterminada para crear graphql.schema.GraphQLSchema. GraphQLSchemaGenerator

Es posible personalizar la generación.

GraphQlSource graphQlSource = GraphQlSource.builder() 
             .schemaResources(..) 
             .configureRuntimeWiring(..) 
             .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> { 
                 // création 
                GraphQLSchema 
             }) 
             .build(); 

3. El RuntimeWiringConfigurer

GraphQL tiene su propio sistema de tipos y utiliza RuntimeWiringConfigurer para hacer la correspondencia de tipos o mapeo.

Spring hace la correspondencia automáticamente entre estos tipos y los tipos Java Spring. El mapeo se realiza a través de un Bean de tipo TypeResolver.

4. Gestión de errores

Podemos usar un Handler de excepción DataFetcherExceptionHandler para centralizar los errores.

Por ejemplo:

 SimpleDataFetcherExceptionHandlerpublic class SimpleDataFetcherExceptionHandler implements  
      DataFetcherExceptionHandler { 
       
         @Override 
         public void accept(DataFetcherExceptionHandlerParameters 
             handlerParameters) { 
             Throwable exception = handlerParameters.getException(); 
             SourceLocation sourceLocation = 
               handlerParameters.getField().getSourceLocation(); 
             ExecutionPath path = handlerParameters.getPath(); 
               ExceptionWhileDataFetching error = new 
               ExceptionWhileDataFetching(path, exception, 
               sourceLocation); 
             handlerParameters.getExecutionContext().addError(error); 
             log.warn(error.getMessage(), exception); 
         } 
      } 
VM6883:64

Conclusión

GraphQL permite crear un API para administrar las consultas con mayor precisión a través de un servicio de discovery, que indica lo que el API puede hacer. En el futuro, seguramente tendremos servidores GraphQL responsivos junto con Server Site Event, que satisfarán una amplia gama de necesidades.

Sin embargo, como cualquier "tecnología emergente", llevará algún tiempo ver si su adopción será amplia o no.

VM6883:64

Puntos clave

  • GraphQL evita tener datos con contenido insuficiente: sub-fetching.

  • GraphQL evita tener datos con contenido excesivo: over-fetching.

  • Spring hace que GraphQL sea fácil de usar.

  • GraphQL permite administrar una suscripción a un flujo de datos.

  • GraphQL es fácil de emparejar con JPA mediante el uso de Spring Data Querydsl. 

  • GraphQL permite llegar más lejos que los servidores RESTful tradicionales.

VM6883:64

El futuro de Spring

Spring ha cambiado de versión principal a principios de año. Tenemos Spring 6 y Spring Boot 3.

Esta versión requiere JDK 17 y funciona con Jakarta EE 9.

Las principales novedades que detallaremos son la compilación nativa Spring Native, el soporte para módulos Java y la observación. Se ha producido una gran limpieza en las funcionalidades obsoletas. Para facilitar la transición, JDK 17 y Jakarka EE 9+ son compatibles con Spring 5.3.x.

La última versión actualmente disponible es 6.0.0-M3 de 17/03/22.spring-framework-6.0.x-stage-milestone-3.1. El resumen de los cambios que hay que realizar en el código para la migración a la versión 6 se indica en esta página: https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x

VM6883:64

Soporte de módulos Java

Podremos utilizar JPMS (Java Platform Module System). Tendremos descriptores de módulos para todos los módulos de Spring. Los módulos han existido desde JDK 9. Se trata de un nuevo nivel de abstracción por encima de los paquetes. Teóricamente, tenemos una nueva abstracción package of Java Packages que permite que el código sea aún más reutilizable. JPMS nopage>Java Platform Module System:Ver JPMS

La adopción del módulo se ha retrasado principalmente debido a los paquetes Maven y de Spring. Las evoluciones se hicieron en Maven (y Gradle), por lo que Spring tuvo que formar parte de este movimiento. Eso se ha hecho.

De hecho, por defecto, no podemos usar la reflexión en clases que importamos de otro módulo, aunque Spring y JUnit utilicen la reflexión de manera masiva.

Con la modularización, tenemos una dependencia tanto durante la ejecución como en el momento de la compilación. Pero Spring nos abstrae de las dependencias en la compilación con el sistema discovery y de configuración automática, que es un enfoque completamente diferente.

Los módulos permiten ocultar ciertas cosas, potenciar el secreto, mientras que Spring está ahí para exponer y reutilizar. Java también suele evolucionar sin tener en cuenta a las personas, las herramientas o quiénes las utilizan, lo que impide o va en contra de ciertos usos, mientras que Spring integra las evoluciones de su ecosistema y abre los diferentes casos de uso tanto como sea posible. 

VM6883:64

Spring Native Spring Native

Desde hace algún tiempo, las empresas han estado migrando al cloud. Los clouds recientes se basan en Docker y Kubernetes. En estas arquitecturas, lo caro es la huella de memoria y el tiempo de lanzamiento de los nodos. Hay soluciones que han comenzado a surgir en este ecosistema, basadas en GraalVM. GraalVM logra competir con los servidores Node.js, especialmente a nivel de los servidores de funciones que escalan a 0. GraalVM

Spring ha colaborado desde el principio con Sun y después con Oracle en el uso de GraalVM y ahora ofrece su integración. Tenemos las ventajas de Quarkus o Micronaut, pero en el ecosistema de Spring. La solución ya está disponible con Spring Boot 2, pero no todos los módulos son compatibles en este momento.

VM6883:64

Proyecto Leyden Proyecto Leyden

Podemos tener la incorporación del proyecto Leyden (The java static graph project). Mark Reinhold, arquitecto del lenguaje Java, propuso mejorar las aplicaciones binarias estáticas para lograr un inicio más rápido y un menor uso de memoria. Esto permitirá compilar código Java (just-in-time) y obtener aplicaciones nativas (ahead-of-time) con capacidades similares al modo nativo de GraalVM. Se trata de la compilación AOT estática.

Spring Observability Spring Observability

Este módulo se basa en Micrometer para las métricas y estará disponible para aplicaciones nativas.

Obsolescencia programada

Es posible que algunas características ya no sean compatibles:

  • Autowiring en los setters por nombre/tipo.

  • Simplificación del FactoryBean.

  • Soporte de los EJB, JCA y de JAX-WS.

  • Configuración de las aplicaciones Spring a través de la configuración XML.

  • Capacidad para realizar llamadas RPC.