Enterprise Integration Patterns y Apache Camel en Spring

En cualquier sistema informático, cada pieza de software, sea grande o pequeña, de una u otra forma se comunica con otras piezas de software. Para obtener información desde alguna base de datos, para enviar mensajes, etc.

Uno de los mayores problemas surge al integrar software gigantes y aquí es donde aparece Apache Camel para solucionar esa problemática.

Pero antes de eso, veamos cuáles son los problemas de trabajar con integraciones.

Problemas de la integración

Comúnmente, una de las soluciones para integrarnos con alguna pieza de software es desacoplar una capa de integración en la aplicación. Esta pieza puede existir como una pieza dentro de nuestra aplicación o tener corriendo una pieza dedicada de software que hace el trabajo, denominada como middleware.

Este middleware puede venir con algunos problemas, por ejemplo:

  • Los canales de información pueden no ser 100% confiables, puede haber fallos al enviar largos volúmenes de datos.
  • El sistema de integración se transforma en un montón de transformaciones de datos y adaptadores para poder soportar toda la variedad de tecnologías. Hay sistemas REST, SOAP, si utilizan mensajería o usan protocolos FTP, esta lista puede crecer hasta el infinito.
  • Los cambios en los formatos de la data y reglas son inevitables. Cada cambio que hagamos en el desarrollo de la aplicación es un cambio que debemos hacer en nuestro middleware.

Para esto, hay soluciones empresariales como los conocidos ESB, que es un Enterprise Service Bus, pero estos son pesados y nunca es tan simple como llegar y utilizar, no tiene la ventaja de una solución más simple, ligera y extensible.

Patrones de integración

Con el paso del tiempo, y como ocurre en el mundo de la tecnología, todas las experiencias de integración ya ha sido recolectada por expertos para evitar rehacer la rueda cada vez que hablamos del tema. Hay un set de templates llamado Enterprise Integration Patterns, utilizado para diseñar flujos de datos.

Estos patrones se crearon en un libro con el mismo nombre, escrito por Gregor Hohpe y Bobby Woolf, y describen 65 patrones para el uso de integración de aplicaciones empresariales y middleware basado en mensajería en la forma de un lenguaje de patrones.

Esto nos ayuda a estandarizar las formas de integrar aplicaciones, tal como dije anteriormente, evitando reinventar la rueda cada vez que tenemos una problemática.

El objetivo de los patrones de integración empresarial es el de crear un lenguaje común y un set de workflows, para poder combinarlos y crear procesos maduros y prácticos.

Más información sobre los patrones pueden encontrarla aquí: https://www.enterpriseintegrationpatterns.com/index.html

También pueden utilizar esta herramienta online https://online.visual-paradigm.com/diagrams/features/enterprise-integration-patterns-diagram-tool/ que les permitirá ver algunos templates y modificarlos para crear sus propios diseños, como por ejemplo:

Smart Proxy

Apache Camel

Apache Camel es un framework middleware orientado a mensajes, e implementa el listado de los Enterprise Integration Patterns de los que hablamos anteriormente, es decir, hace uso de los patrones, soporta todos los protocolos comunes de transporte y tiene una larga lista de adaptadores incluído.

Apache Camel te permite manejar un gran número de rutinas de integración sin tener que escribir código, por lo que en pocas palabras facilita la interacción entre una gran y variable cantidad de tecnologías.

Las conexiones entre servicios y tecnologías se llaman routes. Las rutas son implementadas en el CamelContext y se comunica con los exchange messages.

Apache Camel – Terminología

  • Message: Es una entidad usada por los sistemas para comunicarse entre ellos.
  • Exchange: Encapsula un mensaje y proporciona interacción entre los sistemas. Es el contenedor de los mensajes que determina el tipo del mismo.
  • Camel Context: Es el centro del modelo de Camel que proporciona acceso a servicios como Routes, Endpoints, etc.
  • Routes: Es una abstracción que permite a los clientes y servidores trabajar independientemente. Creamos rutas con lenguajes de dominio específicos (DSL) son una cadena de llamadas a funciones.
  • DSL: Son procesadores, endpoints que están conectados entre ellos al escribirlos con DSL, que al final forman rutas. En nuestro caso, DSL es la API de Java, pero en otros lenguajes o frameworks, podría ser XML u otros.
  • Processor: Realiza operaciones de intercambio. Podemos pensar que las rutas son unidades lógicas que conecta a los procesadores correctos para procesar un mensaje.
  • Component: Son unidades de extensión de Apache Camel. Son las unidades que permiten que Camel se integre tan fácilmente con otros sistemas, trabajan como una fábrica de Endpoints al crearlos con la URI proporcionada. Acá hay un listado de todos los componentes de Apache Camel https://camel.apache.org/components/latest/index.html.
  • Endpoint: Son los puntos de conexión de los servicios que conectan sistemas entre ellos. Creamos endpoints a través de componentes con una URI. Por ejemplo, para crear una conexión FTP podemos proporcionar una URI similar en una route:

ftp://[username@]hostname[:port]/directoryname[?options] 

  • Producer: Son las unidades de Camel que crean y envían mensajes a un endpoint.
  • Consumer: Son las unidades de Camel que reciben mensajes creados por producer, los coloca en wrappers y los envía a los processors.

Ejemplo en código

Vamos a ver un pequeño ejemplo en código para que se entienda mejor todo lo que hemos revisado.

Tenemos como ejemplo, una aplicación que tiene productos y descuentos, que serán automáticamente aplicados a los productos dentro de algunos periodos de tiempo.

Entidad Product y Discount

@Entity
@NamedQuery(name = "discounted-products", query = "select product from Product product where product.discount IS NOT NULL")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private Long price;
    private Long discount;

}

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Discount {

    @Id
    @GeneratedValue
    private Long id;

    private Long amount;

    @OneToOne
    private Product product;
}

Entidad básica para producto, lo único extraño es que tiene una NamedQuery que puede ser llamada desde Camel con su name y retornaría la query pedida, es decir, todos los productos que tengan descuentos.

Por otro lado, creamos la entidad Discount, que es bastante simple, sólo id, monto, y una relación con Product.

A esto, hay que crearles repositorios con Spring Data para poder consultar todo a la base de datos:

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}

@Repository
public interface DiscountRepository extends JpaRepository<Discount, Long> {
}

Hasta ahora, esto es todo muy básico, por lo que no entraré mucho en detalles.

Y finalmente, una clase Service, que llamará a los repositorios antes creados para hacer todas las operaciones:

@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {

    private final ProductRepository productRepository;

    @Override
    public Product findById(Long id) {
        return productRepository.findById(id).orElse(null);
    }

    @Override
    public List<Product> findAll() {
        return productRepository.findAll();
    }

    @Override
    public void save(Product p) {
        productRepository.save(p);
    }
}

Este service es muy simple, sólo llama al Repository, y tiene creados los métodos para obtener un producto por Id, listar todos los productos y guardar un producto.

Por otro lado, tenemos el Service de Discount:

@Service
@RequiredArgsConstructor
public class DiscountServiceImpl implements DiscountService {

    private final DiscountRepository discountRepository;
    private final ProductService productService;

    @Override
    public Discount makeDiscount() {

        Discount discount = new Discount();
        Integer discountRate = new Random().nextInt(100);
        discount.setAmount(discountRate.longValue());

        //traemos uno de los id precargados de producto
        Integer productId = new Random().nextInt(3) + 1;
        Product product = productService.findById(productId.longValue());

        //Seteamos el descuento a un producto y guardamos la entidad
        Long discountedPrice = product.getPrice() - (discountRate * product.getPrice() / 100);
        product.setDiscount(discountedPrice);
        productService.save(product);

        discount.setProduct(product);
        return discount;

    }

    @Override
    public Discount findDiscount(Long id) {
        return discountRepository.findById(id)
                .orElse(null);
    }
}

Este service, tiene dos métodos, uno encargado de generar un descuento aleatorio para un producto aleatorio y guardarlo en la base de datos, y otro método que encuentra un discount por su identificador.

Ahora, creemos unos datos por defecto para nuestra aplicación:

@Component
@RequiredArgsConstructor
public class ProductLoader implements CommandLineRunner {

    private final ProductRepository productRepository;
    @Override
    public void run(String... args) throws Exception {
        Product product = new Product(1L, "Mouse", 2000L, null);
        Product product2 = new Product(2L, "Comic Book", 5000L, null);
        Product product3= new Product(3L, "Notebook", 30000L, null);
        productRepository.save(product);
        productRepository.save(product2);
        productRepository.save(product3);
    }
}

Con eso ya tenemos lo básico. Ahora en nuestro yml, configuremos el ContextPath que necesitamos para Camel. Además, añadimos unas properties de descuento que necesitaremos en nuestras routes.

camel:
  component:
    servlet:
      mapping:
        contextPath: /nullpointerexception/*

discount:
  newDiscountPeriod: 2000
  listDiscountPeriod: 6000

Integremos con Apache Camel

Ya tenemos configurada la data, es hora de integrar Camel a nuestro proyecto. Primero crearemos Routes, para ello utilizaremos la clase base RouteBuilder para crear routes. Sólo debemos extender de dicha Clase para referenciar a los objetos y sobrescribir el método configure():

@Component
public class TimedJobs extends RouteBuilder {
    @Override
    public void configure() throws Exception {
        from("timer:new-discount?delay=1000&period={{discount.newDiscountPeriod:2000}}")
                .routeId("make-discount")
                .bean("discountServiceImpl", "makeDiscount")
                .to("jpa:tech.nullpointerexception.cameldemo.entity.Discount")
                .log("Created %${body.amount} discount for ${body.product.name}");
    }
}

Acá estamos creando rutas usando Java DSL. Acá estamos usando el timer Component de Camel, el cual tiene configuraciones de delay y ejecución periódica. Luego, estamos definiendo una route, que podrá ser referenciada después. Más adelante, estamos llamando a nuestro método makeDiscount, dentro de nuestro bean DiscountServiceImpl. El mensaje es intercambiado y consumido por el logger para loggear la información.

Podemos añadir otra route abajo para listar todos los productos con sus precios actualizados:

 from("jpa:tech.nullpointerexception.cameldemo.entity.Product"
                + "?namedQuery=discounted-products"
                + "&delay={{discount.listDiscountPeriod:6000}}"
                + "&consumeDelete=false")
                .routeId("list-discounted-products")
                .log(
                        "Discounted product ${body.name}. Price dropped from ${body.price} to ${body.discount}");

Estamos utilizando el componente JPA de nuestra entidad de Product y llamando a nuestra namedQuery. Además configuramos nuestro JPA con un delay, así habrá un descuento creado antes de listar los productos. ConsumeDelete significa que no queremos borrar la entidad Product procesada.

Si ejecutamos la aplicación ya podemos ver los resultados:

Log de nuestra aplicación

Crear endpoints

Ya tenemos configurado nuestro componente para lanzar nuestras funciones. Vamos ahora a integrarlos con endpoints REST y generar la documentación Swagger. Creamos una nueva route:

@Component
public class RestApi extends RouteBuilder {

    @Override
    public void configure() {
        restConfiguration()
                .contextPath("/helloworld")
                .apiContextPath("/api-doc")
                .apiProperty("api.title", "Rest API to Test Camel")
                .apiProperty("api.version", "1.0")
                .apiProperty("cors", "true")
                .apiContextRouteId("doc-api")
                .port("8080")
                .bindingMode(RestBindingMode.json);

        rest("/products").description("Details of products")
                .get("/").description("List of all products")
                .route().routeId("products-api")
                .bean(ProductServiceImpl.class, "findAll")
                .endRest()
                .get("discounts/{id}").description("Discount of a product")
                .route().routeId("discount-api")
                .bean(DiscountServiceImpl.class, "findDiscount(${header.id})");
    }
}

Acá estamos creando nuestra API con el nombre contextPath y HelloWorld. Luego el apiContextPath está configurado para api-doc, que es usado por Swagger. Finalmente, si ahora visitamos

http://localhost:8080/helloworld/api-doc

Deberíamos poder ver el Swagger de nuestra aplicación.

Java Collections

Otro tema en el que creo falta más expertiz a todos los que trabajamos con código y en Java, es al manejo de la API Collections. Todos sabemos de la interfaz List y ArrayList. Pero, ¿sabemos que es una LinkedList, un Stack o un Queue?

Este post tiene como objetivo ver toda este framework que proporciona una arquitectura para almacenar y manipular grupos de objetos.

La API Collections proporciona las interfaces Set, List, Queue y Deque y las clases ArrayList, Vector, LinkedList, PriorityQueue, HashSet, LinkedHashSet y Treeset.

Jerarquía de la API Collections.

Como podemos ver en la imagen, hay una jerarquía, y todos tienen la interfaz Iterable, esto permite que podamos recorrer estos grupos de colecciones. Además, la Interfaz Collection proporciona muchos métodos en común para todos los demás elementos:

Métodos Interfaz Collection

Esta Interfaz Collection, extiende de la Interfaz Iterable, que es la base de todas las classes de Collection. Sólo contiene un método abstracto:

Interfaz Iterable

Como podemos ver, esta Interfaz sólo contiene un método abstracto Iterator, y finalmente, ese Iterator, es una interfaz que contiene 3 métodos:

Interfaz List

La interfaz list es la interfaz hija de la Interfaz Collection. Como características importantes:

  • Estructura en la que se puede almacenar una colección ordenada de objetos.
  • Puede tener datos duplicados.
  • La interfaz es implementada por las clases: ArrayList, LinkedList, Vector y Stack.

Vamos viendo ahora estas clases una por una.

List <String> stringList= new ArrayList();  
List <String> strList = new LinkedList();  
List <Long> longList = new Vector();  
List <Example> exampleList = new Stack();  

Clase ArrayList

Características principales:

  • Permite almacenar datos duplicados de elementos de distintos data types.
  • Mantiene el orden de inserción y es de tipo non-synchronized.
  • Los elementos de los ArrayList pueden ser accedidos aleatoriamente.
  • Es un Array redimensionable que aumenta su tamaño según se necesite.

Clase LinkedList

Características:

  • Internamente usa una lista doblemente vinculada para almacenar los elementos. Esto es, que cada uno de los elementos tiene un puntero al anterior y al siguiente elemento.
  • Puede almacenar elementos duplicados.
  • Mantiene el orden de inserción y es non-synchronized.
  • Bajo ciertas ocasiones es mejor el rendimiento.

Clase Vector

Utiliza un array dinámico para almacenar los elementos.

  • Es similar a ArrayList, pero es synchronized.
  • Puede ser más lento que los arrays estándar, pero puede ser útil en programas donde se necesite mucha manipulación en los arrays.

Clase Stack

Stack hereda de Vector. Tal como crees implementa una estructura de pila, es decir, Last-In-First-Out.

  • Contiene los mismos métodos que Vector y adiciona otros útiles como: peek(), push(object o).

Cuando hablamos de non-synchronized nos referimos que si dos o más hilos acceden de forma concurrente al mismo no asegura la integridad de la información.

Interfaz Queue

La interfaz Queue mantiene un orden de tipo FIFO. Podemos definirla como una lista ordenada que mantiene elementos que serán procesados.

Las clases que implementan la interfaz Queue: PriorityQueue, Deque, ArrayDeque.

Queue<String> priorityQueue = new PriorityQueue();  
Queue<String> arrayDeque = new ArrayDeque();  

Veamos las clases que implementan esta infertaz.

Clase PriorityQueue

  • Implementa la interfaz Queue.
  • Mantiene los elementos u objetos que serán procesados por su prioridad.
  • No acepta valores nulos a ser almacenados en la cola.

Un ejemplo:

public static void main(String[] args) {
        Queue<String> priorityQueue = new PriorityQueue<>();
        priorityQueue.add("Gonzalo");
        priorityQueue.add("Munoz");
        priorityQueue.add("Mendez");

        //Contiene algunos métodos útiles como element que muestra el elemento que viene
        System.out.println(priorityQueue.element());

        //elimina el elemento que viene
        priorityQueue.remove();

        System.out.println(priorityQueue.poll());
    }

Y la consola muestra:

Interfaz Deque

  • Extiende de Interfaz Queue.
  • La ventaja que tiene es que podemos remover y añadir los elementos desde ambos lados.
  • Su nombre proviene de dounble-ended queue.

Clase ArrayDeque

  • Esta clase implementa la interfaz Deque. Nos facilita utilizar la Deque.
  • A excepción de queue, podemos añadir o borrar elementos desde ambos lados de la cola. Los métodos que ayudan a esto: addFirst(Object o) y addLast(Object o).
  • Es más veloz que un ArrayList y un Stack y no tiene restricciones de capacidad.

Interfaz Set

  • Representa un set de elementos sin ordenar y no nos permite elementos duplicados.
  • Podemos almacenar como máximo un valor nulo.
  • Es implementada por clases HashSet, LinkedHashSet y TreeSet.
Set<String> s1 = new HashSet<>();  
Set<Example> s2 = new LinkedHashSet<>();  
Set<Integer> s3 = new TreeSet<>();  

Clase HashSet

  • Implementa la interfaz Set.
  • Representa una colección que utiliza una tabla hash para el almacenaje.
  • Contiene elementos únicos.
HashSet<String> setExample=new HashSet<String>();  
set.add("1");  
set.add("2");  
set.add("3");  
set.add("1"); 

Esto nos retorna todos los elementos únicos.

Clase LinkedHashSet

  • Representa la implementación LinkedList para la interfaz Set.
  • Es similar a HashSet, la diferencia es que usa una lista doblemente enlazada para almacenar data y retener el orden de los elementos.

Interfaz SortedSet

  • Es una interfaz optativa a Set que proporciona una ordenación total de los elementos.
  • Los elementos en SortedSet están ordenados en orden ascendente.
  • Proporciona los métodos adicionales que inhiben el orden natural de los elementos.

Clase TreeSet

  • Es la clase que usa la interfaz SortedSet.
  • Contiene elementos únicos.
  • Están almacenados en orden ascendente.
SortedSet<String> ts
                = new TreeSet<>();

        ts.add("Gonzalo");
        ts.add("Zon");
        ts.add("Alberto");
        Iterator<String> itr = ts.iterator();
        while (itr.hasNext()) {
            System.out.println(itr.next());
        }

Esto nos ordena los elementos y el resultado es:

Java Generics

Los genéricos en Java están a partir del SDK 5.0 del lenguaje, y a pesar de llevar tanto tiempo con nosotros, aún hay muchos creadores de código que no saben utilizarlos correctamente.

Un pequeño ejemplo de genéricos

Es muy común que tengamos la necesidad de almacenar objetos en listas para su uso.

Por ejemplo, tomemos un listado con nombres:

List names = new ArrayList();
names.add("Gonzalo");
names.add("Andres");

Ahora, sabemos que esta lista tiene Strings, porque nosotros la declaramos con sólo nombres, verdad? Entonces hagamos alguna iteración e imprimamos los resultados, esto debería hacerse así:

for(String name: names){
    System.out.println("Name " + name);
}

o quizás:

names.forEach((String name) -> System.out.println(name));

Verdad? Bueno, la verdad es que el compilador nos va a lanzar un error en estos casos. No hay un contrato que nos asegure que cada elemento de esa lista es un String, por lo que en nuestros for y forEach lo que tenemos no es un String, sino que un Object que podemos castear para obtener nuestro esperado String:

for(Object name: names){
    String obtainedName = (String) name;
    System.out.println("Name " + obtainedName);
}

names.forEach(name -> System.out.println((String) name));

Este cast tiene varios problemas, en primer lugar, es molesto tener que hacer casting cada vez que necesitamos una variable, y por otro lado, si por equivocación metemos un valor numérico u otro objeto a la lista y hacemos un cast, nos encontraremos con una excepción.

Este problema se soluciona fácilmente si declaramos entre <> el tipo que queremos tenga nuestra lista.

List<String> names = new ArrayList();
names.add("Gonzalo");
names.add("Andres");

for(String name: names){
    System.out.println("Name " + name);
}

names.forEach(name -> System.out.println( name));

Bienvenido a los genéricos!

Clases genéricas

Veamos una clase Java

public class GenericClassTest<T> {

    T object;


    GenericClassTest(T object){
        this.object = object;
    }

    public T getObject(){
        return this.object;
    }

    public static void main(String[] args) {
        GenericClassTest<Long> longGenericClassTest = new GenericClassTest<>(29L);
        System.out.println("Long Generic Test Class: " + longGenericClassTest.getObject());

        GenericClassTest<List<String>> listGenericClassTest = new GenericClassTest<>(Arrays.asList("Gonzalo", "Munoz"));
        listGenericClassTest.getObject().forEach(System.out::println);
    }

}

Analicemos el código, acá en la declaración de la clase estamos agregándole el uso de genéricos. Esto hará que acepte cualquier tipo de objeto.

En los atributos de clase definimos un object de tipo T, y creamos un constructor que inicializará esta variable.

Finalmente estamos haciendo un método getter para obtener este objeto.

Más abajo, en la misma clase he declarado un método main donde hacemos una instancia de nuestra clase, primero con un Long, y luego con un listado de String, e imprimiendo estos valores por consola para ver que nuestra clase genérica funciona de las mil maravillas.

También podemos agregar más genéricos a una clase:

public class GenericClassTwoParameters<T,U> {

    T obj1;
    U obj2;

    GenericClassTwoParameters(T obj1, U obj2){
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    /**
     * Simple method to retrieve the generic data
     */
    public void getParameters(){
        System.out.println("Obj1: "+obj1);
        System.out.println("Obj2: "+obj2);
    }

    public static void main(String[] args) {
        GenericClassTwoParameters<String, Integer> genericClassTwoParameters = new GenericClassTwoParameters<>("Manzanas", 20);
        
        genericClassTwoParameters.getParameters();
    }
}

Funciones Genéricas

También podemos escribir funciones genéricas que pueden ser llamadas con diferentes tipos de argumentos a partir de lo que le pasemos al método genérico.

 static <T> void genericMethod(T element){
        System.out.println(element);
    }

    public static void main(String[] args) {
        //Veamos como se comporta este método:
        genericMethod("Hello World!");

        genericMethod(2000L);

        genericMethod(2.0);
    }

Acá podemos apreciar que nuestro método genérico le estamos pasando un String, luego un Long y finalmente un Double, y la salida por consola:

Algunas características de los métodos genéricos:

  • Antes del tipo de retorno tienen un type parameter.
  • El type parameter puede ser acotado: Esto es cuando ponemos por ejemplo que los tipos aceptados deban ser numéricos:
public <T extends Number> void generic(T element){...}
// Y esto también se acepta
public <T extends Number & Comparable> void generic {...}
  • Los métodos genéricos también pueden tener distintos type parameters:
public static <T, U> List<T> fromArrayToList(T[] a) {...}

Java Message Service (JMS)

¿Qué es JMS?

JMS es un servicio de mensajería en Java. Es una API en Java que permite a una aplicación enviar mensajes con otras aplicaciones.

Conceptualmente pensemos en JPA, en donde ésta es una API estándar e Hibernate es la implementación, esto es similar a JMS, donde ésta es una API estándar que requiere una implementación subyacente a ser proporcionada.

JMS es altamente escalable y permite evitar el acoplamiento entre aplicaciones usando mensajería asíncrona.

Algunas implementaciones JMS:

  • Amazon SQS
  • Apache ActiveMQ
  • JBoss Messaging
  • RabbitMQ
  • Entre muchas otras.

¿Por qué utilizar JMS?

Si bien es cierto que esta mensajería se podría hacer vía REST, JMS proporciona algunas ventajas al ser un servicio específico de mensajería.

Algunas ventajas son:

  • Es asíncrono.
  • Tiene una mejor performance que utilizar protocolo HTTP.
  • Hay flexibilidad en la entrega de mensajes.
  • Es muy confiable, posee una gran robustez cuando hablamos de seguridad.

Tipos de Mensajería

Hay algunos tipos de mensajería que debemos revisar:

Mensajería Point
  • El mensaje es encolado y entregado a un consumidor.
  • Se puede tener múltiples consumidores, pero el mensaje debe ser entregado sólo una vez.
  • Los consumidores se conectan a una cola.
MENSAJERÍA Publish / Subscribe
  • El mensaje es entregado a uno o más suscriptores.
  • Los suscriptores se suscribirán a un tópico, y luego recibirán una copia de todos los mensajes enviados a dicho tópico.

Términos Clave

  • JMS Provider: Es la implementación JMS utilizada.
  • JMS Client:Es la aplicación que envía o recibe mensajes desde un JMS provider.
  • JMS Producer or Publisher: Es el JMS Client que envía mensajes.
  • JMS Consumer / Subscriber: Es el JMS client que recibe mensajes.
  • JMS Message: Es la entidad de la data enviada.
  • JMS Queue: Es la cola de los mensajes point to point. Algunas veces es FIFO.
  • JMS Topic: Similar a una cola, pero para publicar y suscribir.

Mensaje JMS

Un mensaje JMS, contiene tres partes:

  • Header: Contiene metadata del mensaje.
    • JMSCorrelationID (Para tracear un mensaje entre múltiples consumidores).
    • JMSExpires: Se puede setear que un mensaje será borrado después de algún tiempo.
    • JMSMessageId: Seteado por el JMS Provider.
    • JMSPriority: Prioridad del mensaje.
    • JMSTimestamp: Fecha en que se envía el mensaje.
    • JMSType: Tipo de mensaje
    • JMSReplyTo: Queue o tópico donde el sender está esperando respuestas,
    • Otras más…
  • Properties: Las propiedades vienen en tres secciones:
    • Application: De la aplicación Java que envía el mensaje.
    • Provider: Usada por el proveedor JMS y la implementación específica.
    • Standar Properties: Definido por la API JMS.
  • Payload: El mensaje.

Hay muchos campos de Application Properties, Standar Properties y Headers que puede tener un mensaje JMS. Acá solo se revisaron algunos.

Tipos de Mensaje JMS

  • Message: Sólo un mensaje, sin payload. Usado para notificar sobre eventos.
  • BytesMessage: El Payload es un array de bytes.
  • TextMessage: El message está almacenado como un string como un json o xml.
  • StreamMessage: Una secuencia de primitivos Java.
  • MapMessage: El mensaje es nombre en pares de valores.
  • ObjectMessage: El mensaje es un objeto java serializado.

Hoy en día los payloads más populares son los JMS TextMessages, ya que están desacoplados de Java, y un JSON puede ser consumido por cualquier otra tecnología.

Manos a la obra

Ya sabiendo un poco más sobre JMS, veamos qué tal se ve en código.

Vamos a partir creando un nuevo proyecto Spring a partir de un Spring Initializr:

Creación de proyecto Spring
Creación de Proyecto Spring – 2
Creación de Proyecto Spring – Dependencias

Hasta aquí lo único nuevo que tiene es que estamos seleccionando una librería de Spring para que utilicemos ActiveMQ.

Con esto ya estamos listos con nuestro entorno de trabajo:

Proyecto Inicial.

Para nuestro proyecto, querremos levantar un servidor embebido sobre el cual probar las características de JMS, para ello debemos agregar unas nuevas dependencias en nuestro pom:

        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>artemis-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>artemis-jms-server</artifactId>
        </dependency>

Listo!!!

Lo primero que haremos es crearnos un POJO para nuestro mensaje JMS:

Sólo creamos un POJO simple, dentro de un nuevo package model con dos campos. A todo esto, con lombok, le generamos los getters y setters, el patrón builder y los constructores con y sin argumentos. Este POJO no sólo enviará mensajes, sino que los recibirá, por lo cual, también implementamos un Serializable.

Haremos una configuración básica para levantar nuestro servidor MQ Server. Para esto, lo haremos dentro del método main y hacemos las configuraciones básicas:

Todo esto sale directamente desde la documentación de ActiveMQ sobre cómo levantar un servidor embebido. Lo que hicimos fue configurar un pequeño y básico servidor ActiveMQServer.

Ojo que esto es sólo para fines educativos, generalmente se tendrá un servidor MQ externo, fuera de Spring Boot y sólo buscamos poder enviar un mensaje JMS y recibir uno.

Task Configuration

Ahora que nuestro servidor de pruebas está levantado, configuraremos un ejecutor de tareas para Spring, y lo que lograremos será que estaremos mandando un mensaje periódicamente.

Creamos una nueva clase de configuración con las anotaciones @EnableScheduling y @EnableAsync estas anotaciones configurarán Spring para hacer tareas desde un task pool. La anotación @Configuration, le está diciendo a Spring que tome esta clase de configuración, y escanee los métodos anotados como @Bean.

Así tendremos nuestro Bean Task Executor inyectado al Spring Context, y así Spring utilizará esto para ejecutar tareas por nosotros de forma periódica.

Message Converter Configuration

Crearemos ahora una nueva clase llamada JmsConfig. Acá declararemos un Bean llamado messageConverter en el Spring Context. Y esto será un mapper de Jackson a mensaje, específicamente para trabajar con la librería Jackson JSON.

Lo que hará Spring con esto, es que cuando enviemos un mensaje a JMS, Spring lo convertirá a un mensaje de texto de JMS, y el payload tomará ese objeto Java y lo convertirá a JSON plano.

Eso es lo que esta configuración está haciendo, habilita nuestra instancia Spring para que realice esta conversión, y lo mismo cuando llega un mensaje JMS de tipo texto, lo convierta de vuelta a un objeto Java.

Enviar mensajes JMS

Lo primero, es tener seteado un nombre para nuestra cola, esto puede hacerse en la misma clase de configuración de JMS:

Ahora crearemos una clase encargada de enviar los mensajes:

¿Qué está pasando aquí?

Hemos marcado la clase como un @Component Spring, e inyectado un JmsTemplate (que está configurado para hablar con nuestra instancia ActiveMQ). También, tenemos un método llamado SendMessage, que está configurado para ejecutarse cada 2 segundos.

Dentro de este método estamos creando un nuevo elemento de nuestro POJO HelloWorldMessage, y construyéndolo con nuestro Builder de Lombok.

Finalmente, le estamos diciendo a JmsTemplate, que haga la conversión (que hicimos anteriormente en el Message Converter. Y diciéndole que convierte este archivo a tipo Text y lo envíe a la cola que hemos definido.

Si ejecutamos nuestra app, podemos ver en la consola algo como esto:

¡¡¡¡Está funcionando!!!!

Recibir JMS Messages

Ya vimos como enviar mensajes, y lo que nos gustaría ahora es, poder capturarlos. Lo que debemos hacer es configurar un Message Listener, y esto es muy simple dentro de Spring.

Veamos qué hicimos aquí. Primero, creamos una nueva clase y la marcamos como @Component, luego creamos un método listen y lo anotamos como @JmsListener y como parámetro le dimos el nombre de nuestra cola, a la que se conectará.

En la declaración del método vemos que recibiremos un HelloWorldMessage, los headers del mensaje. La anotación @Payload le indica a Spring que puede deserializar el objeto, así como la anotación @Headers le indica que puede extraer los headers del mensaje.

Finalmente, sólo estamos llamando al POJO e imprimiéndolo, si ejecutamos la aplicación ahora nos dará en la consola que se envió el mensaje e instantáneamente, lo recibirá:

Finalmente el objeto Message que no explicamos es sólo el objeto JMS como tal. Si ejecutamos un debug e inspeccionamos los elementos podemos ver lo siguiente:

Conclusiones

Ya hemos visto lo que puede hacer JMS por nosotros. Esto es muy interesante, y sólo dimos un vistazo a todo el potencial que tiene. Próximamente, haré la parte dos de este documento, con ejemplos más complejos, levantando un ActiveMQ en Docker y conectándolo con Spring y veremos comunicación entre micro servicios y más.

Repositorio Github

Finalmente, les dejo el repo de github donde está el ejemplo funcional:

https://github.com/gonzaloan/jmsdemo.git

Go – Hello World

Desde que inicié mi carrera me he especializado en Java, y a su vez, había tenido algunos acercamientos a Python, lenguaje que me encanta por su sencillez y poder.

Hace unas semanas surgió una nueva posibilidad laboral, en un trabajo en el cual se me asignaría por proyectos, entregándome una gran posibilidad de crecimiento profesional, me pareció que era una oportunidad que no podía desaprovechar, así que sin más, acepté la oferta laboral, y estoy a poco más de una semana de entrar a esta nueva empresa.

La sorpresa de esto, es que me asignarán un proyecto en un lenguaje que desconocía totalmente, que es el lenguaje de Google: Go. Si bien ya había escuchado bastante del mismo, nunca había entrado a conocerlo como tal, y este último tiempo me he dedicado a aprender más y más sobre el lenguaje y sus beneficios. Es por esto que hoy hago un pequeño post para mostrar básicamente cómo funciona y qué es lo que nos puede ofrecer.

¿ Qué es Go?

  • Es un lenguaje de programación multiplataforma. Es compilado (el resultado del código fuente es un archivo binario que se puede desplegar en un servidor o computador) e imperativo (cada línea de código es una instrucción).
  • Deriva su sintaxis de C, con algunas diferencias.
  • Tiene tipado estático, es decir, cuando se declara una variable, se define qué tipo de variable es.
  • Trae características de lenguajes interpretados, por ejemplo, no sólo permite el tipado estático, Go también puede inferir qué tipo de variable se está declarando.

¿ Qué beneficios hay al utilizar Go?

  • Es fácil de aprender, y su sintaxis es simple comparado con C.
  • Tiene un uso de CPU eficiente y es veloz. Esto también es porque es compilado.
  • Hace fácil el manejo de concurrencia, la cual es parte nativa del lenguaje, a diferencia por ejemplo de Java.
  • Las librerías estándar de Go te permiten desarrollar una aplicación sin necesitar librerías externas. Puedes hacer un servidor http, o usar sockets sin tener que recurrir a nada más que el lenguaje en sí mismo.
  • Tiene un linter definido e integrado al lenguaje.
  • Tiene un fácil despliegue, al ser compilado, sólo se debe subir el binario al servidor y ejecutarlo.

¿ Qué empresas lo utilizan?

  • Google
  • Docker
  • Canonical
  • Heroku

El primer Hello World

Ahora vamos al ejemplo básico de todo programador novato en un lenguaje, nuestro querido primer Hola Mundo. Para esto obviamente necesitamos descargar Go desde su página web: https://golang.org/

La instalación es tan simple como dar a Siguiente, Siguiente y Finalizar. Una vez listo esto, ya podremos utilizar el CLI de Go, y verificar la instalación abriendo una terminal y escribiendo:

go version

Lo cuál debería darnos como salida la versión que tenemos instalada del lenguaje:

Con esto listo, ya podemos empezar a trabajar, pueden utilizar su IDE predilecto, en mi caso soy amante de Jetbrains y sus soluciones, por lo que utilizo GoLand:

Una vez creada una carpeta de nuestro Proyecto, agregamos nuestro primer archivo: main.go

Y el código para hacer nuestro Hello World:

package main

import "fmt"

func main() {
	fmt.Println("Hello World" )
}

Explicaciones de cada línea de nuestro código

Esto se ve simple, verdad? vamos ahora a analizar cada línea del código para entenderlo un poco mejor.

package main

Package representa a un proyecto o un workspace (un conjunto de archivos fuente), entonces, por ejemplo, si tenemos tres archivos fuente y todos pertenecen al workspace main, es necesario que indiquemos mediante un import que pertenecen allí.

En el caso de por qué usamos main, es para especificar que es un package ejecutable, entonces, si usamos por ejemplo import package blabla, hacemos un go build, esto no nos dará ningún ejecutable como resultado, por lo tanto, si queremos tener código que podamos ejecutar usaremos main, y si no, en el caso de crear código que pueda ser usado con otros fines, o por otros desarrolladores, es que se utilizarán otros nombres de packages.

Si utilizamos el package main, debe siempre tener una función llamada main.

import “fmt”

Acá se está diciendo básicamente que se le entregue al package main acceso a todo el código y funcionalidades dentro del paquete llamado FMT.

En este caso, FMT, es el nombre de una librería estándar incluída en el lenguaje Go por defecto. Viene de la palabra “format”, y tiene funcionalidades usadas para imprimir un montón de información específicamente a la terminal.

func main()

Func declara una función main, que es la función principal que se ejecutará al ejecutar el script.

Dentro de esta función está la llamada a la librería que importamos anteriormente fmt y el método al que queremos llamar, Println, que va a mostrar por consola el mensaje que le entreguemos mediante las comillas dobles que recibe como parámetro.

Ejecutando el código

Una vez tenemos esto listo, por consola podemos lanzarlo así:

go run main.go

Dándonos como resultado:

Y eso es todo!, ya tenemos nuestra primera aplicación con Go.

La tecnología evoluciona, súbete tu también

Al ser desarrolladores, ingenieros de software, arquitectos, o de cualquier área asociada a la tecnología, es importante permanecer relevante en lo nuevo que va saliendo y dominar muchas cosas. La información que obtuvimos en la universidad ya se considera antigua, y si no nos hacemos cargo quedaremos deprecados y perderemos valiosas oportunidades laborales.

Si bien es imposible estar al día en todo lo que va apareciendo en el mundo de TI, es una buena estrategia escoger algunas áreas que nos gusten e interesen, por ejemplo, apuntar a la tecnología que seamos especialistas, en mi caso por ejemplo Java y su framework Spring, o quizás en alguna especialidad que deseemos obtener, como Big Data o Machine Learning.

Siempre estar aprendiendo

En este artículo, veremos formas para lograr esto:

Lecturas: La importancia de leer

“Mientras más leas, más cosas sabrás. Mientras más aprendas, a más lugares podrás ir.” – Dr. Seuss

Libros

Una de las formas más interesantes para aprender nuevas cosas o actualizar las que ya sabemos consiste en leer buenos libros. Comprar libros físicos es bastante fácil hoy en día, tenemos plataformas donde podemos conseguirlos como Amazon, BuscaLibre, y muchas más, pero el problema que existe con esto, es que los libros también se desactualizan muy rápido, y cuando una nueva versión de la tecnología es liberada, o se hace popular otra forma de hacer las cosas, se vuelven obsoletos. A mi me encantan los eBooks.

Mis preferencias:

  • PacktPub.com: Es un sitio en el cual por 10 dólares mensuales puedes tener acceso a un sinfín de libros relacionados a la tecnología, como lenguajes de programación, desarrollo de videojuegos, seguridad, arquitectura y más. Lo mejor de todo es que siempre están saliendo nuevos títulos, por lo que la información disponible siempre está fresca. Estos libros puedes leerlos por la plataforma de PacktPub, o puedes descargarlos como PDF, o EPUB para tu Kindle. Hay también Learning Paths y proyectos que puedes crear paso a paso.

Hay otras plataformas como OReilly.com, pero a mi me encanta PacktPub por su genial biblioteca y su bajo precio.

Blogs

Otra muy buena opción es leer blogs, hay una cantidad inimaginable de personas que buscan compartir sus conocimientos, experiencias, éxitos y fracasos.

Personalmente me gusta buscar blogs, e ir guardándolos en mi Pocket: https://app.getpocket.com/, para leerlos cuando tengo tiempo.

Entre mis blogs favoritos está Medium, DZone y Baeldung (Para los Java Lovers).

Redes Sociales

Las redes sociales no existen sólo para pelear en comentarios de publicaciones de noticieros, una de las cosas interesantes de las redes sociales, específicamente Facebook, es la cantidad de grupos que existen para compartir el conocimiento. Muchos de estos, entregan tutoriales, guías e incluso ofertas laborales.

Yo sigo a bastantes, por ejemplo:

  • Programadores e Informáticos Chile
  • Python Code
  • Just Javascript

y la lista es interminable.

De la lectura a la escritura

Hace un tiempo, comencé mi propio Blog, uno de los miedos iniciales es que se cree que sólo los expertos deberían escribir artículos, o que quizás lo que escriba no iba a ser tan interesante, pero hay que pensar que la escritura tiene un primer beneficiario, que es quien lo escribe. ¿Por qué? para escribir es necesaria hacer una investigación, leer, y en ese proceso aprendes muchísimo sobre el tema.

Dime y lo olvidaré, enséñame y recordaré, involúcrame y aprenderé. – Benjamín Franklin.

Además, enseñar es una forma muy eficaz de aprender y memorizar algo: https://gananci.org/como-memorizar-rapido/

Escucha!

Otra manera de aprender cosas interesantes es a través de escucharlas. Puedes oír Podcasts, Charlas de Youtube.

Personalmente me gusta poner vídeos en Youtube e ir escuchándolos cuando manejo o cuando camino hacia el trabajo en la mañana, sigo canales interesantes como las charlas de Platzi, Charlas TED, etc.

Estudia Online

Con el auge de la tecnología llegaron de buena manera los cursos en línea. Si bien es complejo que estos cursos sean muy profundos, es un buen inicio para conocer una nueva tecnología, y una buena forma de mezclar la teoría con la práctica.

Hay un sinfín de cursos y plataformas, entre las que más me gustan están Coursera, Udemy y Platzi.

Practica y ten tus proyectos

Proyectos de ejemplo

Crea proyectos, experimenta nuevas tecnologías y juega. Utiliza una plataforma como Github para tener tus proyectos personales. Además te servirán cuando busques oportunidades laborales y necesites de algún respaldo de tus conocimientos.

Contribuir en proyectos Open Source

Contribuir en proyectos OpenSource es una muy buena opción para dar a conocer tu talento y practicar. A pesar de que yo no estoy muy relacionado con esto, puedes aprender muchísimo de cómo programan y trabajan los mejores ingenieros de todo el mundo.

Practicar ejercicios de código

Hay muchas plataformas que te permiten solucionar ejercicios de lógica y algoritmos. Una de mis favoritas es HackerRank, te permite realizar una infinidad de problemáticas, prepararte para entrevistas técnicas, desarrollar nuevas habilidades, etc.

Aprendizaje Continuo

Muchas veces fallamos al no darnos cuenta que nuestra carrera no es el fin de nuestra educación, sino que sólo el comienzo. Si creamos un hábito de aprender, vamos a fácilmente extender nuestro conocimiento y mejorar nuestras capacidades de obtener un gran empleo, o quizás crear tu propia empresa.

Generalmente, culpamos al tiempo, pero depende de nosotros buscar lapsos para aprender, personalmente, me gusta tomar una hora cada mañana y los viajes a casa para aprender cosas nuevas.

Los invito a seguir creciendo y mejorando!!!!

MapStruct

Permite mapear de forma rápida y segura objetos entre sí, sin tener que codificar a mano.

Generalmente, usamos DTOs para aislar un objeto de negocio de un objeto que se conecta con las bases de datos (@Entity).

Cuando hacemos una transformación de estos objetos, es útil usar MapStruct.

Configuración

<dependency>  
 <groupId>org.mapstruct</groupId>  
 <artifactId>mapstruct</artifactId>  
 <version>1.3.0.Final</version>  
</dependency>

Además, Mapstruct realiza mapeos en tiempos de compilación, ocupa el generate-sources de maven, que genera las clases de forma automática, así que necesitamos agregar un plugin, que funcione también con lombok:

<plugin>  
 <groupId>org.apache.maven.plugins</groupId>  
 <artifactId>maven-compiler-plugin</artifactId>  
 <version>3.5.1</version>  
 <configuration>  
     <source>1.8</source>  
     <target>1.8</target>  
     <annotationProcessorPaths>  
         <path>  
         <groupId>org.mapstruct</groupId>  
         <artifactId>mapstruct-processor</artifactId>  
         <version>${mapstruct.version}</version>  
         </path>  
         <path>  
         <groupId>org.projectlombok</groupId>  
         <artifactId>lombok</artifactId>  
         <version>${lombok.version}</version>  
         </path>  
     </annotationProcessorPaths>  
     <compilerArgs>  
     <compilerArg>-Amapstruct.defaultComponentModel=spring</compilerArg>  
     </compilerArgs>  
     </configuration>  
</plugin>

Configuración en los IDEs

  • Eclipse: Instalar plugin para permitir habilitar el procesamiento de anotaciones Plugin m2e y el plugin de MapStruct
  • IntelliJ: Sólo habilitar el procesamiento de anotaciones: Settings/Build, Execution, Deployment/Compiler/Annotation Processors

Usos

  1. Mapeo Simple

La primera opción es tener un Bean y un DTO iguales.
Por ejemplo:

@Data  
@AllArgsConstructor  
@NoArgsConstructor  
@Builder  
public class Beer {  
  private UUID id;  
 private String beerName;  
 private String beerStyle;  
 private Long upc;  
}

y el DTO:

@Getter  
@Setter  
@NoArgsConstructor  
@AllArgsConstructor  
@Builder  
public class BeerDTO {  
 private UUID id;  
 private String beerName;  
 private String beerStyle;  
 private Long upc;  

}

La forma de mapear es la siguiente:

  • Crear un package de mappers: En nuestro proyecto creamos un package mappers para almacenar todos nuestros objetos que harán el trabajo de mapear.
  • Crear una interfaz Mapper: En este caso creamos una interfaz BeerMapper, con una anotación de Mapstruct: @Mapper.
  • Crear métodos de conversion BeerToBeerDTO y BeerDTOToBeer: Estos son los métodos que van a convertir nuestros objetos.
@Mapper  
public interface BeerMapper {
//Instance nos permitirá usar el mapper en nuestras clases. 
BeerMapper INSTANCE = Mappers.getMapper(BeerMapper.class);  
  BeerDTO beerToBeerDTO(Beer beer);  
  Beer beerDTOToBeerDTO(BeerDTO beerDTO);  
}
  • El INSTANCE nos va a permitir llamar a la instancia del Mapper para utilizar los métodos.

Con esto, al momento de compilar, hará todo el proceso de mapeo automáticamente.

Finalmente, para convertir el objeto Bean a nuestro DTO simplemente hacemos lo siguiente:

BeerDTO beerDto = BeerMapper.INSTANCE.beerToBeerDTO(beer);

Y esto nos retornará el objeto mapeado.

2) Mapeo con campos distintos

El siguiente ejemplo es cuando tenemos un Bean con campos distintos a los que hay en el DTO, Por ejemplo:

public class EmployeeDTO {
private int employeeId;
private String employeeName;
// getters and setters
}

public class Employee {
private int id;
private String name;
// getters and setters
}

Cuando se mapean objetos distintos, se debe configurar el campo de origen a su campo de destino, para ello utilizamos la anotación @Mapping:

@Mapper
public interface EmployeeMapper {
    @Mappings({
      @Mapping(target="employeeId", source="entity.id"),
      @Mapping(target="employeeName", source="entity.name")
    })
    EmployeeDTO employeeToEmployeeDTO(Employee entity);
    @Mappings({
      @Mapping(target="id", source="dto.employeeId"),
      @Mapping(target="name", source="dto.employeeName")
    })
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

Acá básicamente le decimos por ejemplo, que el campo employeeId debe mapearlo a id.

3) Mapeo de Beans con Beans Hijos

Otro ejemplo usual es cuando un Bean tiene una referencia a otros bean:

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    private DivisionDTO division;
    // getters and setters
}

public class Employee {
    private int id;
    private String name;
    private Division division;
    // getters and setters
}

public class Division {
    private int id;
    private String name;
    //Getters and Setters
}

Por ejemplo, en el caso anterior tenemos Employee, que tiene objeto de Division.

Mapper para este Ejemplo

Para este caso, debemos agregar el mapper del objeto Division y al revés.
MapStruct detectará que el objeto que necesita ser convertido tiene otro objeto del cual existe mapeo, hará la tarea por nosotros.

Solo debemos agregar el Mapper que no tenemos:

DivisionDTO divisionToDivisionDTO(Division entity);
Division divisionDTOtoDivision(DivisionDTO dto);

Y con esto, ya podemos convertir el objeto.

Otros

Hay varias opciones más que explicaré más adelante.

Presentación

Soy Gonzalo, bienvenido al Blog.

Soy de profesión Ingeniero en Computación e Informática y tengo varios años ya en el mundo de la tecnología. Me especializo en Java y Spring, aunque también disfruto de tecnologías como Python y Javascript.

¿ Por qué hago esto?

  • El camino recorrido es largo, planeo dejar tips sobre cómo hacer ciertas cosas, técnicamente hablando, y para ayudar a quien lo necesite. (Quizás también me ayude a mi mismo hacer todo esto 🙂 )
  • Planeo también plasmar mis experiencias para todo aquel que se vea o haya visto en la misma situación mía.

Hace algunos años, me consideraba mediocre, tenía un trabajo fijo y muy estable y dejé de aprender, llegaba en la mañana a sentarme a mi escritorio y esperar la hora de salida. En una especie de epifanía cambié mi forma de pensar, cambié mi forma de ser, y mi vida cambió (Bueno, eso da para otra entrada del Blog). Ahora quiero ayudar a quien lo necesite para encontrar su camino.

Para dar contexto, empecé mi carrera profesional trabajando como PMO, para luego entrar en el área de Desarrollo de Software, he trabajado en varias empresas, algunas grandes y otras no tanto, he conocido a mucha gente y tenido muchas experiencias. Profesionalmente, me he especializado en Java y su framework Spring.

Así que mi plan es plasmar mucha información útil, mostrar algo de la documentación que yo mismo he creado y juntado con el pasar de los años, y contar experiencias de vida.

Cualquier cosa, estoy abierto para conversar con cualquiera,

Saludos!

Gonzalo M.