¿Sabés cómo agregar funcionalidades a Liferay a través de OSGi?

A partir de la version 7 de Liferay el CMS empezó a utilizar el framework OSGi para toda su infraestructura, lo que cambió algunos aspectos del desarrollo de portlets y nos dio nuevas herramientas para modificar o extender su comportamiento de base. Enfoquémonos un poco en entender que es OSGi y como podemos utilizarlo como desarrolladores para Liferay.

¿Qué es OSGi y cómo funciona?

OSGi nació con la finalidad de agregarle modularidad dinámica a Java, y busca hacerlo de una manera liviana, agregando metadatos al Manifest de los jars que contengan nuestras aplicaciones. Organizar nuestro código en módulos nos permite exponer únicamente las clases que queramos que estén disponibles para que otros usen como dependencias. OSGi además nos permite registrar nuestros módulos como servicios, manejar su ciclo de vida (iniciarlos, pararlos, actualizarlos) y sus dependencias. De esta manera podemos resolver un problema utilizando pequeños módulos cohesivos que trabajan en conjunto, facilitándonos la reutilización de funcionalidad y ayudándonos a descomponer el problema en problemas mas pequeños. Además, como desarrolladores de Liferay nos puede surgir la necesidad de sobreescribir algún comportamiento, por ejemplo para realizar acciones cuando un usuario inicia sesión, y necesitamos entender como funcionan estos módulos para lograr nuestro objetivo.

¿Que hay en el manifest?

En el manifest podemos encontrar la metadata que necesita el framework para identificar el módulo que desarrollemos. Liferay utiliza BNDTools para configurar los módulos en vez de editar el manifest directamente. Cuando creamos un módulo se crea un archivo bnd.bnd similar a este:

Bundle-Name: Hola Mundo Api
Bundle-SymbolicName: com.somospnt.osgi.holamundo.api
Bundle-Version: 1.0.0
Import-Package: org.osgi.framework;version="[1.1,1.1.9]",
org.osgi.framework
Export-Package:  com.somospnt.osgi.holamundo.api;version=1.0.0

El Bundle-Name nos da el nombre del módulo, y el Bundle-SymbolicName debe ser único para identificar el módulo dentro del service registry de OSGi. A traves del Bundle-Version declaramos que version del módulo estamos desarrollando. Con Import-Package podemos indicar si necesitamos que otro módulo este presente para que el nuestro funcione, e incluso especificar las versiones que nos sirven del mismo. Finalmente con Export-Package podemos indicar que paquete y version estamos disponibilizando.

Ciclo de vida

Cuando desplegamos un módulo OSGi a un contenedor (en el caso de Liferay es Apáche Felix) este pasará por distintos estados: Primero se da la instalación, en esta fase el módulo se instala en el contenedor pero todavía no se tienen en cuenta las dependencias que este posea. Luego viene la fase de resolución en la cual se verifica si todas las dependencias del paquete están disponibles, veremos las reglas más adelante cuando hablemos de las versiones. Una vez que el módulo esta en estado resuelto podemos iniciarlo y su estado pasará a activo. Si quisiéramos pararlo el módulo volverá al estado resuelto y quedará disponible para iniciarse posteriormente. 

Sistema de versiones

Cuando especificamos una dependencia o exportamos un paquete, OSGi nos permite especificar, que version o rango de versiones necesita nuestro componente.

Imaginemos que tenemos una nueva version de una api que cambia la firma de uno de los métodos, lo cual provocaría que los clientes de la misma dejen de funcionar. OSGi nos permite tener instaladas diferentes versiones de un mismo módulo y al momento de iniciarlo se resuelven las dependencias tomando la version mas alta compatible (si no se especifica una version tomará la version mas alta disponible).

Ejemplo

Como las dependencias se resuelven luego de la instalación y no cuando iniciamos o paramos nuestro módulo, si desplegamos una nueva version de nuestro módulo no afectaremos a los clientes actuales hasta que los reinstalemos, pudiendo incluso agregar un nuevo cliente que consuma la nueva version y que coexista con los anteriores.

Supongamos que tenemos un módulo helloworld.client que no especifica la version de helloworld que necesita, y otro módulo helloworld.anotherclient que especifica las versiones [1.1.0,1.1.9].Además contamos con 3 versiones de helloworld, la 0.0.0, la 1.0.0 y la 1.2.0 entonces las dependencias quedarían de la siguiente manera al instalar los módulos clientes: 

¿Como lo usa liferay?

Liferay utiliza el registro de servicios de OSGi y nos permite definir para cada servicio una api, proveedores y consumidores para poder desacoplar los clientes de las implementaciones.

Esta separación en módulos diferentes entre la api y la implementación, nos permite desacoplar las implementaciones y poder actualizarlas sin reinstalar los clientes en caso de ser necesario. Dicho esto, OSGi nos permite crear el módulo con la implementación directamente y queda a nuestro criterio como diseñamos la separación en nuestros módulos. 

Veamos un ejemplo con un simple servicio para saludar con la siguiente interfaz:

public interface SaludoService {
    public void saludar(String nombre);
}
Api

Como vimos, para tener un módulo debemos tener los metadatos para el manifest en el bnd.bnd

Bundle-Name: Hola Mundo Api
Bundle-SymbolicName: com.somospnt.osgi.holamundo.api
Bundle-Version: 1.0.0

Para indicar que SaludoService es una api podemos hacer uso de una de las anotaciones de bndTools: @ProviderType que indica que todas las clases que implementen esa interfaz serán proveedores, el archivo nos quedaría así:

package com.somospnt.osgi.holamundo.api;
import aQute.bnd.annotation.ProviderType;

@ProviderType
public interface SaludoService {
    public void saludar(String nombre);
}

Desplegando el módulo tendríamos disponible la api.

Implementación

Primero el bnd.bnd

Bundle-Name: Hola Mundo Impl
Bundle-SymbolicName: com.somospnt.osgi.holamundo.impl
Bundle-Version: 1.0.0

Notemos que no hace falta exportar la implementación ya que el runtime de osgi va a inyectar la adecuada en tiempo de ejecución: El cliente solo va a conocer la api!

Nota: si usamos maven para compilar este proyecto debemos agregar como dependencia el proyecto que contenga la api

Veamos como quedaría la clase implementadora entonces:

package com.somospnt.osgi.holamundo.impl;
import com.somospnt.osgi.holamundo.api.SaludoService;

import org.osgi.service.component.annotations.Component;

@Component(
    immediate = true,
    property = {
    },
    service = SaludoService.class
)
public class SaludoServiceImpl implements SaludoService {

    @Override
    public void saludar(String name) {
        System.out.println("Hola " + name + "!");
    }

}

En ese código podemos ver una nueva anotación @Component que registra nuestra implementación en el runtime de OSGi, veamos que significan las 3 opciones que le pasamos:

immediate: indica si el módulo debe cargarse de inmediato o ser cargado cuando se use (lazy-load). En este caso queremos que se cargue de inmediato por lo que lo seteamos en true. property: nos permite agregar una lista de propiedades como string. No la necesitamos para el ejemplo. service: indica que api estamos implementando.

Con estos 2 pasos tendríamos un nuevo servicio en Liferay, al que podemos acceder desde otros módulos o desde, por ejemplo, un webcontent.

Cliente

Para cerrar la idea veamos como podríamos consumir este servicio desde otro módulo. Por razones de simplicidad vamos a crear un módulo que nos permita ejecutar un comando desde el Gogo Shell al que podemos acceder en Liferay desde

Panel de control -> Configuración -> Consola de Gogo

Arranquemos como siempre con el bnd.bnd

Bundle-Name: modulo_saludar_cliente
Bundle-SymbolicName: com.somospnt.saludo.client
Bundle-Version: 1.0.0

Nuevamente en nuestro bundle no vamos a importar ni exportar nada, pero recordemos agregar como dependencia en maven a la api para la compilación.

package com.somospnt.saludo.client;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import com.somospnt.osgi.holamundo.api.SaludoService;

@Component(
        immediate = true,
        property = {
            "osgi.command.scope=saludar",
            "osgi.command.function=saludar"
        },
        service = SaludoCommand.class
)
public class SaludoCommand {

    @Reference
    private SaludoService saludoService;

    public void saludar(String name) {
        SaludoService greeting = saludoService;
        greeting.saludar(name);
    }

}

Como nueva anotación importante tenemos @Reference que le indica al framework de OSGi que debe inyectar un proveedor en tiempo de ejecución, si conocemos acerca de Spring sería similar al @Autowired.

También tenemos cambios en las opciones de component. OSGi nos pide que siempre declaremos de que tipo es la clase que estamos agregando, como en este caso solo estamos consumiendo un servicio no necesitamos especificar una interfaz y podemos usar la clase propia como service. (Incluso podríamos ser mas genéricos y utilizar Object) Las properties agregadas le indican a osgi como se utilizará el comando, que tiene la sintaxis

[scope]:[function] argumento

El scope define un espacio de nombres y la funcion nos indica que método de nuestra clase estará disponible, en caso de tener mas de uno podemos declarar varias propiedades osgi.command.function.

Luego de desplegar nuestro modulo podemos usarlo desde la Consola de Gogo y obtenemos el siguiente resultado:

En este primer acercamiento a OSGi pusimos ver los conceptos fundamentales del framework y agregar una pequeña funcionalidad a Liferay con la estructura típica de api, proveedor y consumidor que usan los proyectos de Liferay. En siguientes artículos nos adentraremos en los tipos de proyectos que Liferay nos facilita para resolver problemas comunes. Van a poder encontrar un workspace de ejemplo con el código en el repositorio de Github. ¡Hasta la próxima!

Mandanos tus sugerencias

Ayudanos con ideas para los artículos de este blog a contacto@somospnt.com

¡Seguínos en nuestras redes sociales para enterarte de los últimos posts!