Una clave en la transformación digital de nuestros clientes, además de acompañarlos durante todo el proceso, es comenzar poco a poco modernizando sus herramientas y procesos, consiguiendo cambios disruptivos con un esfuerzo controlado que no impliquen hacerlo todo desde cero. Un ejemplo de esto sería la reutilización de recursos ya existentes y consolidados en el cliente a los que darles un nuevo valor añadido; nuevas formas de uso, funcionalidades, etc.

Este post se centra en la creación de un servicio de generación de informes a partir de las plantillas que ya tiene el cliente, concretamente JasperReports. Los objetivos son que cada sistema de información no tenga que implementar sus propios mecanismos para generar informes, aprovechar los conocimientos del propio cliente en la creación de plantillas y explotar las ventajas que nos ofrece la computación en la nube. Utilizaremos la solución serverless AWS Lambda, pero es extensible a la tecnología FaaS de cualquier otro proveedor cloud.

JasperReports, ese viejo conocido

Si preguntamos a cualquier desarrollador Java por algún sistema para elaborar informes a partir de plantillas, la respuesta está clara: JasperReports. Es verdad que hay otras librerías con similares características, pero JasperReports lleva entre nosotros desde 2001 y esto lo convierte en un veterano asentado en el mercado. De este modo, es muy probable que los sistemas de información de nuestro cliente, que puedan tener más de una década, utilicen JasperReports. A lo largo de los años habrán cambiado su imagen de marca, los tipos de informes e incluso pueden que hayan ido actualizando las versiones de la librería. Esto nos posiciona en una situación clave, en la que con un pequeño esfuerzo podemos implementar un nuevo sistema cloud de generación de informes aprovechando todas las plantillas que ya posee el cliente, permitiendo que pueda seguir modificándolas, creando nuevas y todo ello con una herramienta que conoce ampliamente.

A la hora de modernizar o transformar un sistema y prepararlo para el cloud, un paso fundamental es “partir” el monolito. Hay varias formas de hacerlo; entre ellas, por bloques funcionales. Y qué funcionalidad más clara y diferenciada del resto del sistema que la de generación de informes. Es una funcionalidad que se puede encapsular fácilmente y, aunque depende del tipo de sistema, por regla general no es algo que se utilice constantemente; pueden agrupar datos mensuales o anuales; requieren procesados de datos pesados; amplio uso en momentos concretos o se generan con procesos periódicos. Al unir todas las piezas del puzzle vemos de forma clara la solución: serverless.

Dependiendo del proveedor de cloud que utilicemos, o si utilizamos infraestructura propia, seleccionaremos la correspondiente alternativa para implementar la solución. Para este post, utilizaremos una solución basada en AWS. De modo que la arquitectura a alto nivel queda de la siguiente manera:

AWS Lambda  2

Para simplificar, en el diagrama anterior no se han incluido detalles de VPC, subredes, internet gateways, etc.

Los pasos que seguiremos serán los siguientes:

  1. Creación de plantilla IaC para:
  • Crear toda la infraestructura necesaria en AWS.
  • Asociar la función lambda.
  • Creación de la API Gateway integrada con la función lambda.
  1. Creación del código Java para nuestra función lambda.
  2. Creación de informe/plantilla JasperReports.
  3. Subir nuestros recursos (plantilla Cloudformation, función lambda y plantilla JasperReports) a AWS.
  4. Realizar pruebas de nuestra función lambda.

El código fuente utilizado en este post, se localiza en el siguiente repositorio de Github.

Este proyecto se ha basado en otro ya existente en el Github de AWS.

El proyecto original contiene el código de una lambda que genera el informe JasperReports a partir de una conexión a una base de datos RDS. Ha servido gran parte del código, pero se ha modificado la forma de trasladar los datos a la plantilla. Para hacerlo más genérico y reutilizable, en vez de recuperar los datos desde una consulta a base de datos, vamos a procesar el JSON que viaja en la petición POST.

NOTA: antes de nada, destacar que no va a mostrarse el código completo sino que se explicarán las principales partes más interesantes.

Paso 1 - Creación de plantilla IaC

Una forma sencilla para crear todos los elementos de infraestructura que necesitamos para implementar nuestra funcionalidad es utilizar una plantilla de infraestructura como código, IaC. Cada proveedor cloud tiene su propio servicio de IaC, hay soluciones agnósticas al proveedor Cloud, por ejemplo Terraform, y en el caso de AWS este servicio se denomina CloudFormation.

Ya que no es el objetivo de este post analizar en detalle IaC, por lo que sólo vamos a explicar algunos parámetros del fichero de CloudFormation en el que definimos el stack. El fichero está localizado aquí.

En el fichero se han definido varios parámetros para poder configurar el stack a la hora de lanzarlo, que son:

  • FilesS3Location
    Nombre del bucket S3 en el que se localizan los ficheros implicados en la creación de la función y todos sus recursos: ficheros de IaC y empaquetado Java.
  • TemplatesS3Location
    Nombre del bucket S3 en el que se incluyen las plantillas JasperReports (JRXML).
  • LambdaPackagedFile
    Nombre del fichero que contiene el empaquetado de la función lambda. Por defecto es jasperreports-1.0.0.jar.

También se define la función javaJasperLambda en la que destacar los siguientes puntos:

  • Handler: paradigmadigital.com.myLambdaFunctionHandler
    Aquí incluimos la clase que contiene el Handler de la función lambda. Es necesario incluir el paquete completo.
  • Runtime: java8
    Aunque podemos utilizar versiones posteriores, provocan problemas con las fuentes a la hora de generar el informe desde JasperReports. Se puede solucionar incluyendo estas fuentes y librerías adicionales, pero por simplicidad dejamos la versión 8.
  • CodeUri:
    Bucket: !Ref FilesS3Location
    Key: !Join [ '', [ 'lambda/', !Ref LambdaPackagedFile] ]

    Ubicación en la que está nuestro “ejecutable” Java con el código de la función. En nuestro caso subiremos nuestro paquete compilado en un directorio “lambda” dentro bucket S3 que crearemos previamente (lo explicaremos en el paso 4).
    OJO: Al tratarse de una lambda en Java, hay que tener en cuenta que debemos incluir nuestro paquete ya compilado y con todas las dependencias incluidas.
  • MemorySize: 2048
    Esta es la cantidad mínima de memoria con la que vamos a ejecutar nuestra función. Como mínimo requiere 512MB, pero tras realizar varias pruebas de rendimiento, un valor recomendable es 2048. Si ponemos un número menor a 512 dará problemas a la hora de generar el informe ya que no tiene memoria suficiente. Y con valores inferiores a 2048 obtendremos peores tiempos de respuesta.
  • BUCKET_NAME: !Ref TemplatesS3Location
    Consiste en una variable de entorno que se utiliza dentro de nuestra función e indica el nombre del bucket de S3 en el que se ubican las plantillas de JasperReports que estarán disponibles. En este caso se ha incluido una variable dentro del stack, en la que se define el nombre del bucket de S3 en el que están ubicadas las plantillas.

Por otro lado, también definiremos el swagger mínimo de nuestra función para que pueda interpretarlo correctamente el API Gateway y vincule con la invocación a nuestra función.

En este fichero, además de definir la API, se incluye la referencia a la función que se invocará al recibir el API Gateway peticiones en la URL en la que se publica la API. Concretamente, en esta sección URI. que tiene el siguiente valor:

"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:java_jasper/invocations"

Donde son destacables:

  • ${AWS::Region}
    Variable con la región en la que se ejecuta la API. Dependerá de la región en la que se lance la plantilla de CloudFormation. En cualquier caso, se sustituye durante el registro de la API y no es necesario actualizar manualmente este valor.
  • ${AWS::AccountId}
    Similar al campo anterior pero referido a la cuenta de AWS.
  • function:java_jasper/invocations
    Enlace a la función que deseamos lanzar al recibir peticiones en el endpoint.

Paso 2 - Creación del código Java para nuestra función lambda

En el paso anterior hemos definido los elementos que requerirá nuestra función, que no varían mucho de cualquier otra. Ahora nos centraremos en crear nuestra función que sea capaz de:

  • Leer el JSON que viaja en la petición POST, en la que viajan los datos con los que se quiere generar el informe.
  • Leer del bucket de S3 la plantilla de JasperReport correspondiente.
  • Generar la estructura de datos de entrada que requiere la plantilla a partir de los datos que venían en el JSON.
  • Utilizar la librería de JasperReports para elaborar el informe en PDF.
  • Devolver la respuesta en dicho informe.

Aunque se ha tomado de base el código de ejemplo de AWS, se ha modificado el método que procesa la entrada, para recuperar el cuerpo de la petición POST (postBody) y qué plantilla (template) debemos utilizar.

JasperReports requiere que la información que se traslada a los informes sean objetos Java, con su correspondiente tipo de dato. De modo que esta información es necesaria incluirla dentro del body que recibe nuestro endpoint, quedando de la siguiente manera:

"title": "<Título del informe>",
"parameters": {
    "parametro_n": {
        "type": "<Tipo de dato del parámetro>",
        "value": "<valor del parámetro>"
    }, ...

Los tipos de datos de los parámetros son los siguientes (aunque pueden definirse más):

  • String.
  • Double.
  • Date.
  • Boolean.
  • Datasource. Indica que el parámetro es de una fuente de datos, de modo que se incluirán también cada uno sus los registros en formato array.
  • MainDatasource. En este caso, no se definirá como un parámetro más del informe, sino que se establecerá como la fuente de datos principal del informe. También se incluyen los datos asociados a dicho datasource en formato array.

Los registros de un datasource se incluirán de la siguiente manera:

"parameters": {
  "parametro_n": {
    "type": "<Datasource ó MainDatasource>",
      "value": [
        {
          "field_n": {
            "type": "<Tipo de dato del campo>",
            "value": "<valor del campo>"
          }, ...
          ] ...

De este modo, se han realizado grandes cambios en la clase que genera el informe para trasladar el cuerpo JSON de la llamada POST a una estructura de parámetros y objetos que entienda JasperReports.

En primer lugar, se recorren todos los elementos que vienen en el JSON. Y luego, se van añadiendo a un mapa de parámetros según el tipo concreto de cada uno.

Sobre los data sources, ya se pasen como parámetros o el principal del informe, JasperReports requiere objetos del tipo JRDataSource. Hay varias clases que implementan dicha interfaz, entre ellas JREmptyDataSource, que es un data source vacío. Otra implementación es JRMapCollectionDataSource, que utiliza una colección de mapas (clave y valor) y es la que vamos a utilizar en este post. Cada elemento/mapa de la colección corresponde con un registro del data source.

Otro cambio importante que se ha realizado con respecto al repositorio de ejemplo de AWS es sustituir el uso de la Java SDK1 por la versión SDK2. La causa principal de este cambio ha sido la mejora del rendimiento de la nueva versión y el tener un mayor soporte a futuro. Tras este cambio, junto con la ampliación de memoria a 2048MB, se ha logrado un tiempo de respuesta de unos 10 segundos incluyendo el arranque en frío (cold start). Con la versión anterior de la SDK y 512MB de memoria, API Gateway muchas veces daba error de timeout en la primera llamada por tardar más de 30 segundos en hacer el arranque en frío. En las siguientes llamadas (una vez está la lambda cargada) los tiempos varían en función de la complejidad del informe, pero no existen problemas de timeout con el API Gateway. En cualquier caso, siempre se han conseguido mejores resultados con la SDK2.

Paso 3 - Creación de informe/plantilla JasperReports

El siguiente punto consiste en elaborar una plantilla con la que poner a prueba nuestro desarrollo. Los que han tenido contacto con JasperReports hace unos años recordarán el editor iReport. Sin embargo, a día de hoy, ha evolucionado en otra herramienta: Jaspersoft Studio. Aunque está basada en el IDE Eclipse, se instala como una herramienta totalmente independiente.

Crearemos nuestra plantilla con los siguientes parámetros de entrada:

  • title - java.lang.String
  • string1 - java.lang.String
  • double1 - java.lang.Double
  • date1 - java.util.Date
  • boolean1 - java.lang.Boolean
  • test_image - java.lang.String
  • datasource1 - net.sf.jasperreports.engine.data.JRMapCollectionDataSource
AWS Lambda  3

En este caso el datasource lo vamos a pasar por parámetro. También podría pasarse como data source principal en cuyo caso deberíamos definir los campos que forman parte del data source. En nuestro ejemplo, los definiremos en un dataset Dataset1:

  • field1 - java.lang.String
  • field2 - java.lang.Double

Por último, establecemos el diseño de nuestra plantilla utilizando los campos que hemos definido anteriormente.

En el título incluímos todos los campos que pasamos por parámetros salvo datasource1. Al arrastrar cada uno, incluye también una caja de texto con cada nombre, para localizarlos mejor. En el caso de la imagen, establecemos como expresión el valor del parámetro test_image.

AWS Lambda  4

Para terminar, en la sección de detalle incluimos una tabla. Al editar el detalle de la misma, accedemos a definir un subreport en el que incluir como columnas cada uno de los campos del Dataset1 definido previamente.

AWS Lambda  5

Con todo, ya tenemos definido un informe con el que poder probar nueva función lambda. Podemos localizar la plantilla de ejemplo en el repositorio.

Paso 4 - Subir nuestros recursos a AWS

Una vez tenemos listo el fichero de CloudFormation, la definición de la API, el código fuente de la función lambda y la plantilla JasperReports, lo siguiente es incorporarlo todo a AWS. Lo ideal es incorporar todos estos recursos utilizando nuestros sistemas de CI/CD (Jenkins, Gitlab-CI, etc.), modificando las correspondientes plantillas o pipelines.

Como primera aproximación, e independiente de la solución CI/CD, vamos a utilizar unos scripts de shell y Docker para automatizar todo el proceso. Todos los ficheros necesarios están ubicados en el repositorio del proyecto.

Los requisitos son simples: tener una cuenta de AWS y Docker instalado en nuestra máquina local. Se ha elegido Docker para simplificar todo el proceso y no tener que instalar Java, Maven ni AWS-CLI en nuestro equipo local.

Los pasos son los siguientes:

  • Obtener las credenciales de AWS.
  • Modificar el fichero aws_config/credentials con los valores correctos de nuestras credenciales.
  • Revisar el fichero aws_config/config con la región de AWS en la que crearemos todos los recursos.
  • Modificar el fichero lambda_config.sh estableciendo nuestros valores para todas las propiedades. Hay que prestar atención a las propiedades files_s3_bucket y templates_s3_bucket, ya que requieren valores válidos y únicos en todo AWS.
  • Dar permisos de ejecución a todos los ficheros .sh.
  • Ejecutar el fichero 00.launch_all_steps.sh para lanzar el proceso completo o lanzar los pasos uno a uno.
  • Cuando se haya completado el proceso satisfactoriamente, se habrá dejado lanzada la creación del stack de CloudFormation (es un proceso que tarda varios minutos). Es necesario ir a la consola Web de AWS para verificar que la creación ha finalizado correctamente.
AWS Lambda  6

En el anterior diagrama se muestra el flujo utilizado por los scripts:

  1. Compilar y empaquetar el código de la función lambda.
  2. Crear los buckets S3 para los ficheros de la función lambda y las plantillas JasperReports.
  3. Copiar todos los recursos a los buckets S3:
  • Stack de CloudFormation.
  • Swagger para API Gateway.
  • Empaquetado Java con la función lambda.
  • Plantilla JasperReports de prueba.
  1. Lanzar la creación del stack de CloudFormation, a partir del fichero existente en el bucket S3, estableciendo los correspondientes valores a los parámetros (nombre de los buckets y fichero empaquetado) según la configuración establecida en lambda_config.sh.
AWS Lambda  7

NOTA: En el stack existen referencias al resto de ficheros ubicados en los buckets S3 (swagger y empaquetado con la función) que se utilizarán a la hora de crear los recursos.

Paso 5 - Realizar pruebas

Como último paso, utilizaremos Postman para realizar una llamada POST al endpoint que hemos publicado en el API Gateway y que invoca a nuestra función lambda.

Lo primero que debemos hacer es obtener la URL en la que está “esperando” nuestra lambda. Para ello, vamos a la consola Web de AWS, concretamente al API Gateway y vemos qué URL se le ha asignado.

AWS Lambda  8

De este modo, la llamada que debemos hacer para invocar nuestra función lambda es:

  • Método: POST
  • URL: https://XXXXXXXX.amazonaws.com/jasper?template=template

Siendo el parámetro “template” el nombre del fichero que contiene la plantilla que deseamos utilizar y que está ubicada en el bucket S3. Y el cuerpo del mensaje, incluimos todos los datos que deseamos utilizar en nuestro informe.

En el repositorio hay una llamada de ejemplo para trasladar los datos a la plantilla JasperReports de prueba que hemos creado anteriormente. Es un fichero con una colección de postman en lo que lo único que debemos modificar es la URL en la que está la función.

Podemos revisar la estructura del mensaje, en el que para cada campo se indica su tipo y el valor que toma.

AWS Lambda  9

Una vez responda el servicio, salvamos la respuesta como un fichero PDF y lo abrimos para verificar que ha funcionado correctamente.

AWS Lambda  10

NOTA: Hay que tener en cuenta que la primera vez, o cuando lleva tiempo sin utilizarse la lambda, AWS debe hacer el arranque en frío y tardará algo más en responder el servicio.

Cierre

Hemos visto cómo crear un sistema genérico de generación de informes utilizando JasperReports y AWS lambda. Hemos utilizado infraestructura como código (IaC), definición de APIs, procesado el JSON de una llamada POST desde Java, mejorado el rendimiento y migrado desde la SDK1 a la SDK2, creado una plantilla JasperReports y automatizado de manera sencilla todo el proceso de construcción y despliegue utilizando scripts de shell y Docker.

Los siguientes pasos consistirán en comprobar que el servicio acepta las plantillas ya existentes y se pueden generar los informes sin problema. Esto nos permite una primera vía para comenzar con la transformación digital de los clientes, permitiendo que sigan utilizando las plantillas que ya tienen pero ofreciendo un nuevo uso de las mismas e iniciarlos en el camino de la computación en la nube.

Como reflexión podemos decir que transformar no es siempre tirarlo todo y empezar desde cero. Buena parte del trabajo consiste en buscar nuevas formas de hacer las cosas y reutilizar al máximo lo que ya está funcionando correctamente.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.