← Volver al blog
·8 min de lectura

Introduccion a OSGi en Liferay: modulos, servicios y ciclo de vida

LiferayJavaOSGi

Que es OSGi y por que Liferay lo adopto

OSGi (Open Services Gateway initiative) es una especificacion de modularidad para Java que permite dividir una aplicacion en bundles independientes, cada uno con su propio classloader, dependencias explicitas y ciclo de vida gestionado. En terminos simples, OSGi resuelve el problema historico de Java: el classpath monolitico donde todas las clases son visibles para todos.

Liferay adopto OSGi como nucleo de su arquitectura a partir de Liferay DXP 7.0, reemplazando el sistema de plugins basado en WAR que usaban las versiones anteriores. Las razones fueron claras:

  • Modularidad real: Cada funcionalidad (blog, documentos, formularios) es un conjunto de bundles independientes que pueden desplegarse, actualizarse o desinstalarse sin reiniciar el servidor
  • Aislamiento de classloaders: Un modulo no puede acceder a clases de otro a menos que este las exporte explicitamente, eliminando conflictos de dependencias
  • Servicios dinamicos: Los componentes se registran y descubren en tiempo de ejecucion a traves de un registro de servicios, permitiendo extensibilidad sin acoplamiento directo
  • Hot deploy: Los bundles se instalan y activan en caliente, acelerando drasticamente el ciclo de desarrollo

El runtime de OSGi que usa Liferay internamente es Apache Felix, junto con Equinox como alternativa compatible. Sobre este runtime, Liferay agrega su propia capa de herramientas, incluyendo Blade CLI y Liferay Workspace.

Ciclo de vida de un bundle

Cada bundle en OSGi transita por un ciclo de vida definido con seis estados. Comprender estos estados es fundamental para depurar problemas de despliegue:

INSTALLED: El bundle fue instalado en el framework pero sus dependencias aun no se han resuelto. Esto sucede cuando un bundle referencia paquetes que ningun otro bundle exporta.

RESOLVED: Todas las dependencias del bundle fueron satisfechas. El framework verifico que cada Import-Package declarado en el manifiesto tiene un Export-Package correspondiente en otro bundle activo. Un bundle en estado RESOLVED esta listo para activarse pero aun no ejecuta codigo.

STARTING: El bundle esta en proceso de activacion. Si tiene un BundleActivator, su metodo start() esta ejecutandose. Si usa Declarative Services, los componentes se estan registrando.

ACTIVE: El bundle esta completamente activo y sus servicios estan disponibles en el registro. Este es el estado normal de operacion.

STOPPING: El bundle esta siendo detenido. Los servicios se desregistran y el BundleActivator.stop() se ejecuta si existe.

UNINSTALLED: El bundle fue removido del framework. Sus clases ya no estan disponibles.

El error mas comun en Liferay es ver un bundle atascado en INSTALLED cuando deberia estar en ACTIVE. Esto casi siempre indica dependencias faltantes, y lo veremos en la seccion de depuracion.

Creando un modulo OSGi con Liferay Workspace

Liferay Workspace es la estructura de proyecto recomendada. Genera un entorno Gradle preconfigurado con los plugins necesarios para compilar bundles OSGi.

Para crear un workspace desde cero:

blade init -v dxp-2024.q4 mi-workspace
cd mi-workspace

Dentro del workspace, los modulos se organizan en el directorio modules/. Creemos un servicio simple que exponga una API para saludar usuarios:

blade create -t api -p com.ejemplo.saludo -c SaludoService modules/saludo-api
blade create -t service-builder -p com.ejemplo.saludo modules/saludo-service

La estructura resultante separa la interfaz (API) de la implementacion (service), un patron fundamental en OSGi.

El archivo bnd.bnd

Cada modulo tiene un archivo bnd.bnd en su raiz que controla los headers del manifiesto OSGi. Es el archivo mas importante del modulo:

Bundle-Name: Saludo API
Bundle-SymbolicName: com.ejemplo.saludo.api
Bundle-Version: 1.0.0
Export-Package: com.ejemplo.saludo.api

Los headers clave son:

  • Bundle-SymbolicName: Identificador unico del bundle en el framework. La convencion es usar el paquete base
  • Bundle-Version: Sigue versionado semantico. OSGi usa las versiones para resolver dependencias con rangos
  • Export-Package: Paquetes que este bundle hace visibles a otros bundles. Solo las clases en paquetes exportados son accesibles desde fuera
  • Import-Package: Generalmente lo calcula automaticamente el plugin de bnd analizando el bytecode, pero puede configurarse manualmente

Configuracion de build.gradle

El build.gradle de un modulo Liferay es relativamente simple gracias a los plugins del workspace:

dependencies {
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "default"
    compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.5.1"
    compileOnly group: "org.osgi", name: "org.osgi.annotation.versioning", version: "1.1.2"
}

Las dependencias se declaran como compileOnly porque el runtime de Liferay ya las proporciona. Incluirlas como implementation causaria conflictos de classloader al duplicar clases en el bundle y en el framework.

Declarative Services: @Component y @Reference

Declarative Services (DS) es el mecanismo estandar de OSGi para registrar y consumir servicios sin escribir codigo de gestion manual. Liferay lo usa extensivamente.

Definamos primero la interfaz del servicio en el modulo API:

package com.ejemplo.saludo.api;

public interface SaludoService {
    String saludar(String nombre);
    String saludarConTitulo(String nombre, String titulo);
}

Ahora la implementacion en el modulo service, usando la anotacion @Component:

package com.ejemplo.saludo.internal;

import com.ejemplo.saludo.api.SaludoService;
import org.osgi.service.component.annotations.Component;

@Component(
    service = SaludoService.class,
    property = {
        "saludo.tipo=formal"
    }
)
public class SaludoServiceImpl implements SaludoService {

    @Override
    public String saludar(String nombre) {
        return "Hola, " + nombre + ". Bienvenido al sistema.";
    }

    @Override
    public String saludarConTitulo(String nombre, String titulo) {
        return "Estimado/a " + titulo + " " + nombre + ", bienvenido/a.";
    }
}

La anotacion @Component le dice al framework DS que registre esta clase como un servicio de tipo SaludoService en el registro de OSGi. Las propiedades permiten filtrar servicios cuando hay multiples implementaciones.

Para consumir el servicio desde otro componente, usamos @Reference:

package com.ejemplo.web.portlet;

import com.ejemplo.saludo.api.SaludoService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

@Component(
    service = Portlet.class,
    property = {
        "com.liferay.portlet.display-category=category.sample",
        "javax.portlet.display-name=Saludo Portlet",
        "javax.portlet.name=com_ejemplo_web_SaludoPortlet"
    }
)
public class SaludoPortlet extends MVCPortlet {

    @Override
    public void render(
        RenderRequest request, RenderResponse response)
        throws IOException, PortletException {

        String mensaje = _saludoService.saludar("Carlos");
        request.setAttribute("mensaje", mensaje);
        super.render(request, response);
    }

    @Reference
    private SaludoService _saludoService;
}

@Reference inyecta automaticamente la implementacion activa de SaludoService. Si el servicio no esta disponible (porque su bundle no esta activo), el componente que lo referencia tampoco se activara, manteniendo la consistencia del sistema.

Service Ranking: sobrescribir servicios de Liferay

Una de las capacidades mas potentes de OSGi en Liferay es poder reemplazar implementaciones predeterminadas sin modificar el codigo original. Esto se logra con el service ranking.

Cuando multiples componentes implementan la misma interfaz, OSGi entrega el de mayor ranking. Liferay usa ranking 0 por defecto en sus servicios, asi que cualquier valor positivo toma precedencia:

@Component(
    service = SaludoService.class,
    property = {
        "service.ranking:Integer=100"
    }
)
public class SaludoServiceCustom implements SaludoService {

    @Override
    public String saludar(String nombre) {
        return "Que tal, " + nombre + "! Bienvenido.";
    }

    @Override
    public String saludarConTitulo(String nombre, String titulo) {
        return titulo + " " + nombre + ", es un placer.";
    }
}

Con service.ranking:Integer=100, este componente sera inyectado en lugar del original en cualquier punto donde se use @Reference de SaludoService. El servicio original sigue existiendo en el registro pero con menor prioridad.

Este patron es la base para personalizar el comportamiento de Liferay: desde cambiar la logica de autenticacion hasta modificar como se indexa el contenido.

Depuracion con Gogo Shell

Gogo Shell es la consola interactiva del framework OSGi en Liferay. Se accede desde el panel de control (Control Panel > Gogo Shell) o via telnet al puerto 11311.

Los comandos mas utiles para diagnosticar problemas:

# Listar todos los bundles y su estado
lb

# Buscar un bundle por nombre simbolico
lb | grep saludo

# Ver detalle de un bundle especifico (ID 534 como ejemplo)
headers 534

# Ver por que un bundle no resuelve
diag 534

# Ver servicios registrados por un bundle
inspect cap service 534

# Ver que servicios requiere un bundle
inspect req service 534

# Forzar la resolucion de un bundle
resolve 534

# Reiniciar un bundle
stop 534
start 534

El comando diag es el mas importante. Cuando un bundle esta en INSTALLED en lugar de ACTIVE, diag muestra exactamente que paquetes faltan. Por ejemplo:

com.ejemplo.saludo.service [534]
  Unresolved requirement: Import-Package: com.ejemplo.saludo.api; version="[1.0.0,2.0.0)"

Esto indica que el bundle saludo-api no esta desplegado o no exporta la version correcta del paquete.

Errores comunes y como evitarlos

ClassNotFoundException en tiempo de ejecucion: Ocurre cuando un paquete se usa en el codigo pero no esta declarado en Import-Package. Aunque bnd lo calcula automaticamente, puede fallar con clases cargadas por reflexion. La solucion es agregar el import manualmente en bnd.bnd:

Import-Package: com.paquete.oculto,*

El asterisco final es importante: le dice a bnd que mantenga todos los imports que calculo automaticamente ademas del que agregamos.

ServiceException - componente no se activa: Si un @Reference apunta a un servicio que no existe, el componente completo falla silenciosamente. Usar el scope optional cuando el servicio no es estrictamente necesario:

@Reference(cardinality = ReferenceCardinality.OPTIONAL)
private ServicioOpcional _servicio;

Conflictos de version: Dos bundles exportando el mismo paquete con versiones incompatibles. La solucion es alinear versiones en los build.gradle y usar rangos de version apropiados.

Split packages: Un mismo paquete Java exportado por dos bundles distintos. OSGi no permite esto. La solucion es reorganizar el codigo para que cada paquete pertenezca a un unico bundle.

Conclusiones

OSGi le da a Liferay DXP una flexibilidad que pocos portales empresariales ofrecen. La capacidad de desplegar, actualizar y reemplazar funcionalidades en caliente, con aislamiento real entre modulos, es lo que permite que plataformas Liferay escalen a cientos de personalizaciones sin convertirse en un monolito inmantenible.

Las claves para trabajar efectivamente con OSGi en Liferay son: separar siempre API de implementacion, usar Declarative Services en lugar de activadores manuales, entender el ciclo de vida de los bundles y dominar Gogo Shell para diagnosticar problemas. Con estos fundamentos, el desarrollo de modulos personalizados pasa de ser frustrante a predecible.