Gestión de dependencias en módulos Liferay 7

Publicado por Antonio Javier Ortega Pérez el

PortalesLiferayOSGiGradle

Una de las grandes novedades a nivel de arquitectura en Liferay 7 ha sido la incorporación de OSGi en el centro del software. En Liferay 7, excepto los temas y layouts, todo, absolutamente todo, es un módulo OSGi. Ya no existe el concepto de hook o ext tan conocido en Liferay 6, ahora son módulos OSGi que sobrescriben o publican una nueva funcionalidad. Incluso la capa de retrocompatibilidad con Liferay SDK acaban siendo módulos web WAB (Web Application Bundle), que básicamente son módulos web (WAR) convertidos a OSGI. Es por esta razón que es de vital importancia el saber gestionar correctamente los módulos OSGi, y específicamente, ser muy consciente de que se está desplegando y cómo se gestionan sus dependencias.

En este artículo se escenificará un ejemplo muy típico en las empresas y es el caso de disponer de un backend existente con el cual nos tenemos que integrar. Este backend existente no se puede modificar, y publica un API muy concreto que queremos reutilizar 100% sin modificar y el cual, evidentemente, tiene unas dependencias (que es ahí donde está el ‘juego’ ;) ) . El ejemplo será muy sencillo pero representativo. Este artículo no pretende ser un tutorial de cómo crear / consumir servicios, como crear portlets, etc. sino comentar lo que nos vamos a encontrar cuando gestionemos dependencias en módulos para Liferay 7.

Punto de inicio

Partiremos de una infraestructura compuesta por tres módulos basados en una entidad cualquiera llamada ‘foo’. Los tres módulos son: fooCommon, fooServer y fooClient y todos ellos utilizan Spring. La infraestructura de modelo (foo) se basa en el principio HATEOAS, donde las entidades, aparte de contener los datos, también indican que acciones se pueden realizar sobre ellas:

  • fooCommon: Esquema de datos de intercambio de los servicios. Son los datos que publica el proyecto foServer y que consume fooClient.
  • fooServer: Publica servicios REST.
  • fooClient: Consume los servicios REST.

Dentro del proyecto fooCommon solo tendremos la clase Foo, que es la que intercambian los servicios. Notar que esta clase tiene unas ‘peculiaridades’.

  • La clase hereda de ‘ResourceSupport’, la cual es una clase del paquete spring-hateoas
  • El constructor utiliza anotaciones @JsonCreator y @JsonProperty

Dadas estas dos características podemos afirmar que, aún que la clase es un bean, no es un POJO, dado que tiene acoplamientos con spring-hateoas y Jackson (no es una clase Java ‘pura’).

Objetivo

Dado el escenario planteado y el punto de inicio, nuestro objetivo será crear uno o más portlets que consuman uno o más servicios (proporcionados por el proyecto fooServer) utilizado el API de fooCommon, y empaquetando los desarrollos en varios módulos OSGi de forma eficaz y eficiente. Este objetivo se implementará de varias formas a fin de mostrar diferentes procedimientos y los pros y contras de cada opción.

Como ocurre en múltiples ocasiones el procedimiento utilizado en un desarrollo puede difuminar el objetivo de este. En el caso de desarrollo para Liferay 7 típicamente utilizaremos Liferay IDE, gradle (o maven) y bnd, pero notar que lo que al final vale y ‘va a misa’ es el módulo OSGI generado. Si en alguna ocasión algo no funciona como esperábamos es de utilidad inspeccionar el módulo generado y revisar exactamente que contiene, en especial el fichero META-INF\ MANIFEST.MF.

Tiempo de compilación vs tiempo de ejecución

En cualquier módulo en general y en este en ejemplo en particular, para poder llamar al servicio necesitaremos tener accesible las clases que necesita. Notar que al mencionar ‘tener accesible’ siempre hay que tener en cuenta dos conceptos:

  • En tiempo de compilación:

Este hecho dependerá de cómo estemos programando, pero, en un entorno de desarrollo ‘típico’ de Liferay 7 será con gradle y Liferay IDE. Esto quiere decir que las dependencias deberán estar explicitadas en gradle (directamente, o indirectamente como dependencias transitivas) y trasladadas a Eclipse para que este sea consciente de ellas (seguramente mediante la opción Gradle -> Refresh Gradle Project).

En tiempo de ejecución: En este punto se pueden dar dos casos:

  • Que las dependencias estén empaquetadas directamente dentro de nuestro módulo (mediante compileInclude de Liferay para Gradle, mediante las propiedades de la tarea bundle de gradle, la directiva Include-Resource de OSGi, etc).
  • Que las dependencias se cojan de un módulo OSGi desplegado en la infraestructura que exporte los paquetes necesarios.

Pongamos un ejemplo sencillo de este caso. Hacemos un módulo de Liferay que necesita instanciar la clase Foo. Para ello deberemos añadir al fichero build.gradle la dependencia respecto al artefacto foo-commons, que es donde se almacena la clase Foo que devuelve el servicio. Si añadimos al fichero build.gradle la dependencia mediante la siguiente línea:

compile group: "org.atsistemas", name: "fooCommon", version: "0.0.1-SNAPSHOT"

Al desplegar el módulo vemos que se da el siguiente error:

Total time: 6.872 secs
stop 509
update 509
file:/D:/L70\_Platform/workspaces/liferay/testHateoas/modules/foo-
web/build/libs/org.atsistemas.foo.portlet-1.0.0.jar
start 509
org.osgi.framework.BundleException: Could not resolve module:
org.atsistemas.foo.portlet \[509\]
Unresolved requirement: Import-Package: org.atsistemas.testhateoas.model
Updated bundle 509

Este es el típico error que se da cuando tenemos una dependencia en Gradle (tiempo de compilación) pero no en bnd.bnd (para OSGi, tiempo de ejecución). Notar que esta dependencia no está ni dentro del paquete ni publicada en ningún módulo OSGi, entonces, en tiempo de ejecución no se puede encontrar.

Incluir dependencias en módulos

En este apartado trataré como se pueden incluir dependencias dentro de un módulo de Liferay. Para ello utilizaré la aproximación más sencilla que es llamar a un servicio directamente des de un portlet. Aunque es la opción más sencilla es la menos recomendable, aun así, nos encontraremos con los típicos problemas de classpath de compilación y dependencias entre módulos.

Incluir las dependencias como módulo UBER

Un ‘Uber module’ es como un ‘Uber JAR’, es decir, un módulo que contiene todas sus dependencias de forma ‘expandida’ (no en JARs) dentro de él mismo. Para incluir las dependencias en bnd solo hay que utilizar la directiva ‘Include-Resource’ de la siguiente forma:

Bundle-Name: foo-web-all-uber  
Bundle-SymbolicName: foo.web.all.uber  
Bundle-Version: 1.0.0  
Export-Package: foo.web.all.constants  
Include-Resource: \  
  @commons-logging-1.2.jar,\
  @fooCommon-0.0.1-SNAPSHOT.jar,\
  @jackson-annotations-2.8.9.jar,\
  @jackson-core-2.8.9.jar,\
  @jackson-databind-2.8.9.jar,\
  @slf4j-api-1.7.25.jar,\
  @spring-aop-4.3.12.RELEASE.jar,\
  @spring-beans-4.3.12.RELEASE.jar,\
  @spring-context-4.3.12.RELEASE.jar,\
  @spring-core-4.3.12.RELEASE.jar,\
  @spring-expression-4.3.12.RELEASE.jar,\
  @spring-hateoas-0.24.0.RELEASE.jar,\
  @spring-web-4.3.12.RELEASE.jar,\
  @spring-webmvc-4.3.12.RELEASE.jar
Import-Package: *;resolution:=optional  

De este punto hay que destacar muchos hechos:

  1. Se tienen que poner explícitamente todas las dependencias. Bnd no resuelve las dependencias (como Maven, Gradle o Ivy), por tanto, no podemos contar con las dependencias transitivas, sino que hay que explicitar todas las dependencias. Esto puede ser relativamente fácil contando con herramientas/comandos como “mvn dependency:tree” o “blade gw dependencies” para consultar el árbol de dependencias.
  2. Para que este método funcione se deben explicitar todas las dependencias en el fichero build.gradle, aún que esta sea una dependencia transitiva de otra. De esta forma el fichero build.gradle quedaría de la siguiente forma:

Una vez ya construido el módulo vamos a ver dos características:

  1. El módulo ocupa 7.154 Kb, lo cual es lógico teniendo en cuenta que se incorporan varias librerías de Spring y Jackson.

Las clases se encuentran descomprimidas dentro del módulo:

  • changelog.txt
  • com
  • content
  • license.txt
  • META-INF
  • notice.txt
  • org
  • OSGI-INF
Incluir dependencias mediante Gradle bundle

La segunda forma de crear un portlet incluyendo las dependencias necesarias para llamar al servicio es parametrizando la tarea bundle de Gradle. En este punto lo más destacado es incluir la propiedad “includeTransitiveDependencies = true” y establecer las propiedades que tendrá el MANIFEST.MF:

dependencies {  
  compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0"
  compileOnly group: "com.liferay.portal", name: "com.liferay.util.taglib", version: "2.0.0"
  compileOnly group: "javax.portlet", name: "portlet-api", version: "2.0"
  compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
  compileOnly group: "jstl", name: "jstl", version: "1.2"
  compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0"

  compile "org.atsistemas:fooCommon:0.0.1-SNAPSHOT"
}

bundle {  
  includeTransitiveDependencies = true

  instructions << [
        'Export-Package' : '!*',
        'Import-Package' : '*;resolution:=optional',
        'Private-Package': '!com.liferay.*, ' +
              '!javax.portlet.*, ' +
              '!javax.servlet.*, ' +
              '!org.apache.taglibs.*' +
              '!org.osgi.*,' +
              '*',
       '-sources'       : false              
  ]
}         

Cabe destacar que cuando se utiliza la tarea bundle de gradle las instrucciones relativas a la construcción del fichero MANIFEST.MF sobrescriben lo que hayamos indicado el fichero bdn.bnd, por tanto, en este fichero solo dejaremos las directivas referentes a la identificación del módulo, dado que la gestión de paquetes la especificaremos en Gradle:

Bundle-Name: foo-web-all-gradleBundle  
Bundle-SymbolicName: foo.web.all.gradleBundle  
Bundle-Version: 1.0.0  

Referente a esta forma de construir el módulo se debe destacar que es necesario poner la instrucción ‘Private packages’ en la tarea bundle de Gradle ya que sino no se incluyen las dependencias. Esto es un poco contradictorio dado que en OSGi ‘puro’ cualquier paquete no marcado como ‘exportable’ se considera un paquete privado.

Como hemos hecho antes, una vez ya construido el módulo vamos a ver sus características:

  1. El módulo ocupa 7.792 Kb, lo cual es lógico teniendo en cuenta que se incorporan varias librerías de Spring y Jackson.

Las clases se encuentran descomprimidas dentro del módulo:

  • com
  • content
  • META-INF
  • org
  • OSGI-INF
  • xmlns
Incluir dependencias mediante compileInclude

‘compileInclude es un nuevo tipo de dependencia añadido por Liferay para Gradle que hace lo que su nombre indica: al generar el módulo de Liferay añade la dependencia indicada y sus dependencias transitivas.

Dada la anterior explicación el fichero build.gradle quedaría de la siguiente forma:

dependencies {  
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0"
    compileOnly group: "com.liferay.portal", name: "com.liferay.util.taglib", version: "2.0.0"
    compileOnly group: "javax.portlet", name: "portlet-api", version: "2.0"
    compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
    compileOnly group: "jstl", name: "jstl", version: "1.2"
    compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0"

    compileInclude "org.atsistemas:fooCommon:0.0.1-SNAPSHOT"
}           

Y el fichero bnd.bnd así:

Bundle-Name: foo-web-all-compileInclude  
Bundle-SymbolicName: foo.web.all.compileInclude  
Bundle-Version: 1.0.0  
Import-Package: *;resolution:=optional  

Notar que, como sabemos que fooCommon tiene como dependencias transitivas Spring y Jackson, no hace falta incluir ninguna dependencia más.

Una vez más vamos a ver como se ha construido el módulo:

  1. El módulo ocupa 6.397 Kb, lo cual es lógico teniendo en cuenta que se incorporan varias librerías de Spring y Jackson.

Las clases se encuentran incluidas como librerías dentro del módulo y referenciadas des del fichero MANIFEST.MF:

  • content
  • lib
  • commons-logging-1.2.jar
  • fooCommon-0.0.1-SNAPSHOT.jar
  • jackson-annotations-2.8.0.jar
  • jackson-core-2.8.9.jar
  • jackson-databind-2.8.9.jar
  • slf4j-api-1.7.25.jar
  • spring-aop-4.3.12.RELEASE.jar
  • spring-beans-4.3.12.RELEASE.jar
  • spring-context-4.3.12.RELEASE.jar
  • spring-core-4.3.12.RELEASE.jar
  • spring-expression-4.3.12.RELEASE.jar
  • spring-hateoas-0.24.0.RELEASE.jar
  • spring-web-4.3.12.RELEASE.jar
  • spring-webmvc-4.3.12.RELEASE.jar
  • META-INF
  • org
  • OSGI-INF
Manifest-Version: 1.0  
Bnd-LastModified: 1516196527105  
Bundle-ClassPath: .,lib/fooCommon-0.0.1-SNAPSHOT.jar,lib/spring-hateoa  
 s-0.24.0.RELEASE.jar,lib/jackson-databind-2.8.9.jar,lib/spring-aop-4.
 3.12.RELEASE.jar,lib/spring-beans-4.3.12.RELEASE.jar,lib/spring-conte
 xt-4.3.12.RELEASE.jar,lib/spring-core-4.3.12.RELEASE.jar,lib/spring-w
 eb-4.3.12.RELEASE.jar,lib/spring-webmvc-4.3.12.RELEASE.jar,lib/slf4j-
 api-1.7.25.jar,lib/jackson-annotations-2.8.0.jar,lib/jackson-core-2.8
 .9.jar,lib/spring-expression-4.3.12.RELEASE.jar,lib/commons-logging-1
 .2.jar
…

Se debe tener presente que el uso de compileInclude interviene en le generación del fichero MANIFEST.MF no solo en la incorporación de librerías, sino también en la gestión de paquetes. Por ejemplo:

  • Si en el fichero bnd.bnd no hemos indicado la directiva “Import-Package:” se asume “Import-Package: *” (incluir todos los paquetes), entonces, se analizarán todos los imports de las clases referenciadas y se añadirán como valores de “Import-Package” en el fichero MANIFEST.MF generado.
  • Si en el fichero bnd.bnd hemos indicado la directiva “Import-Package: *;resolution:=optional” entonces, se analizaran todos los imports de las clases referenciadas y se añadirán como valores de “Import-Package” en el fichero MANIFEST.MF generado, solo que cada uno tendrá la opción “resolution:=optional”.
  • Si en el fichero bnd.bnd hemos indicado la directiva “Import-Package: ” significa no incluir nada, entonces en el MANIFEST.MF del módulo generado no habrá directiva “Import-package” y todos los paquetes se habrán incluido dentro de la directiva “Private-Package:”

Aclaraciones

En los ejemplos mostrados hasta el momento se podría entrar en muchísimo más detalle de lo que se ha dado, pero creo que eso difuminaría el concepto principal que pretendo explicar. Con esos ejemplos simplemente quiero que queden claros unos pocos hechos muy simples:

  • Si el módulo ocupa ‘mucho’ (6~7 Mb) podemos asumir que las dependencias de Spring y Jackson se han incluido correctamente.
  • Las dependencias se pueden incluir de forma expandida o como jars referenciados.
  • Muchas veces, lo que hacemos en gradle o bnd afecta al fichero generado, y, si tenemos dudas, siempre es recomendable consultar como se ha generado, o la información del módulo en la consola OSGi.

¿Cómo estructurar los módulos?

Hasta ahora hemos visto una forma fácil y directa de cómo poner las dependencias necesarias dentro de un módulo, ahora, ¿Cuál es la mejor forma de organizar dependencias y estructurar módulos? Realmente hay muchas formas de hacerlo, pero no todas son ‘óptimas’.

Pensemos en un escenario donde tenemos que realizar una intranet y tenemos 10 o 20 portlets que se comunican con un backend, ¿Para cada uno de ellos tenemos que incrustar todas las librerías? Es decir, ¿Cada módulo con portlets tendrá TODAS las librerías de Spring y Jackson? La respuesta sería algo como ‘Esperaba más de OSGi ...’. Hacer eso implicaría una carga inmensa y gratuita que puede afectar mucho al rendimiento del portal, pero claro, he empezado por lo que NO se debería hacer ;)

A continuación, pondré tres ejemplos de cómo se podría estructurar un escenario así. En los siguientes ejemplos ‘pintaré’ como ‘caja grande’ aquellos módulos pesados (por ejemplo, que ocupen 6/7 Mb) y con ‘caja pequeña’ aquellos módulos ligeros (por ejemplo, 4Kb). Igualmente, en el ejemplo trataré con dos entidades ficticias llamadas Foo y Bar simplemente para plasmar el caso de ‘hay varias entidades’.

Portlets con toda la lógica incrustada

Este es el caso más simple y del cual he mostrado los tres ejemplos iniciales. Cada portlet tiene en sí mismo la llamada al servicio, no se define ningún API y cada portlet incorpora todas las librerías necesarias.

  • PROs
  • Totalmente autocontenido.
  • CONTRAs
  • No se puede reutilizar servicios ni API.
  • Muy pesado (cada portlet 7 Mb)
Portlets sencillos llamando a servicios separados con dependencias

Este realmente sería un ejemplo similar al anterior solo que los servicios en sí mismos se han separado en módulos diferentes.

  • PROs
  • Servicios reutilizables.
  • CONTRAs
  • No se puede reutilizar el API.
  • Muy pesado (cada servicio 7 Mb)
Multicapa

A esta forma de estructurar la he llamado multicapa simplemente porque sigue la filosofía de diseño en N capas lógicas (del cual todos estamos hartos de oir la más conocida, MVC) solo que aplicada a módulos OSGi en Liferay.

El caso anterior es el caso más completo, complejo y flexible. Vamos a analizar los módulos que hay:

  • Infraestructure: Módulo de infraestructura que contiene todas las clases de Spring y Jackson. Este módulo no contendrá ningún código desarrollado por nosotros, sino que solo servirá de contenedor de clases para que quien lo requiera las tenga accesibles. Notar que este módulo:
  • No contiene código directamente (si clases o jars incrustdos)
  • Incluye todas las dependencias necesarias.
  • No importa ningún paquete, es autocontenido
  • Publica todos los paquetes que incluye.
  • *-common: módulos que contiene la información que se intercambia en los servicios. Hay que tener presente que la información que intercambian los servicios son: parámetros de entrada, parámetros de salida y excepciones. En el caso que hemos planteado aquí este módulo solo contiene la clase Foo. Recordar que Foo no era un POJO, sino que heredaba de ResourceSupport y tenía anotaciones de Jackson, por tanto, este módulo necesita (=depende) del de infraestructura.
package org.atsistemas.testhateoas.model;

import org.springframework.hateoas.ResourceSupport;

import com.fasterxml.jackson.annotation.JsonCreator;  
import com.fasterxml.jackson.annotation.JsonProperty;

public class Foo extends ResourceSupport {

    private final Long coreId;

    private final String name;

    @JsonCreator
    public Foo(@JsonProperty("coreId")Long coreId, @JsonProperty("name") String name) {
        super();
        this.coreId = coreId;
        this.name = name;
    }

    public Long getCoreId() {
        return coreId;
    }

    public String getName() {
        return name;
    }
}

Igualmente recordar que en el escenario planteado esta clase formaba parte de un API existente que no podemos tocar, es decir, existe un artefacto (un JAR) con la clase, por tanto, no creamos la clase dentro del módulo, sino que el módulo solo hace un wrapper del artefacto en formato OSGi: incorpora la librería dentro del módulo y marca los paquetes como exportables.

  • *-api: Este módulo definirá el contrato de los servicios (el API). Esta es la misma idea de siempre que ha habido en Java, Spring, etc.: nos casamos con la definición, no con la implementación.
package org.atsistemas.foo.service;

import org.atsistemas.testhateoas.model.Foo;

public interface FooService {

    public Foo findOne(Long coreId);

    public Foo[] findAll ();
}

Notar que el módulo sólo depende de su respectivo módulo *-common, que son los parámetros de entrada, salida y excepciones (por simplificar en este caso solo hay parámetros de salida):

  • *-service: Este módulo es la implementación concreta del servicio, y en este ejemplo he utilizado Spring RestTemplate. Este módulo depende de los siguientes:
  • *-api: Donde se define el API del servicio.
  • *-common: Donde se definen los datos de intercambio.
  • infraestructure: Donde se encuentran las clases de utilidad necesarias para llamar al servicio rest (en este caso, RestTemplate).
  • *-web: Módulo donde se implementan los portlets. En este módulo es donde estarán las clases que directa o indirectamente hereden de Portlet, las JPS’s, etc… Hasta ahora, cuando habíamos hablado de que un módulo ‘depende’ de otro era a nivel de paquete, es decir, un módulo importa un paquete que otro exporta. Aquí se da una dependencia nueva que es a nivel de inyección de componente (como en Spring, CDI, etc.). En este módulo se inyecta la implementación concreta del servicio en la referencia basada en el API. En el esquema general he marcado las dependencias de paquetes con una flecha continua mientras que las referencias de inyección están marcadas con una flecha discontinua. El módulo *-web está acoplado a nivel de paquetes con los módulos *-api y –service, y, en tiempo de ejecución OSGi resolverá la inyección del servicio conforme a la implementación que hay en *-service.

Este es un diseño más orientado a microservicios, que es como Liferay 7 está internamente implementado.

  • PROs
  • API bien definido.
  • Servicios reutilizables.
  • Permite definir varias implementaciones de los servicios.
  • Gran flexibilidad ante cambios.
  • Muy ligero
  • CONTRAs
  • Muchas capas, es muy complejo
package org.atsistemas.foo.portlet;

import java.io.IOException;

import javax.portlet.Portlet;  
import javax.portlet.PortletException;  
import javax.portlet.RenderRequest;  
import javax.portlet.RenderResponse;

import org.atsistemas.foo.constants.FooPortletKeys;  
import org.atsistemas.foo.service.FooService;  
import org.atsistemas.testhateoas.model.Foo;  
import org.osgi.service.component.annotations.Component;  
import org.osgi.service.component.annotations.Reference;

import com.liferay.portal.kernel.portlet.bridges.mvc.MVCPortlet;

@Component(
    immediate = true,
    property = {
        "com.liferay.portlet.display-category=category.sample",
        "com.liferay.portlet.instanceable=true",
        "javax.portlet.display-name=foo-web Portlet",
        "javax.portlet.init-param.template-path=/",
        "javax.portlet.init-param.view-template=/view.jsp",
        "javax.portlet.name=" + FooPortletKeys.Foo,
        "javax.portlet.resource-bundle=content.Language",
        "javax.portlet.security-role-ref=power-user,user"
    },
    service = Portlet.class
)
public class FooPortlet extends MVCPortlet {

    private FooService fooService;

    @Reference(unbind="-")
    public void setFooService(FooService fooService) {
        this.fooService = fooService;
    }

    @Override
    public void doView (RenderRequest renderRequest, RenderResponse renderResponse) 
throws IOException, PortletException {  
        String sFoo = renderRequest.getParameter("foo");

        if ("ALL".equals(sFoo)) {
            Foo[] foaFoos = fooService.findAll();
            renderRequest.setAttribute("foos", foaFoos);
        } else {
            Foo foFoo = fooService.findOne(1l);
            renderRequest.setAttribute("foo", foFoo);
        }
        super.doView(renderRequest, renderResponse);
    }
}
Otros posibles diseños

Aunque aquí he planteado tres posibles diseños hay tantos como queramos. Por ejemplo:

  • Que los portlets implementen la llamada al servicio, pero que las clases necesarias estén en módulo de infraestructura:
  • Que los portlets llamen a servicios, y que estos encapsulen API y clases necesarias y hagan uso del módulo de infraestructura.
  • Etc.

En cualquier caso, no hay una solución buena o mala para todos los escenarios. Planteemos los dos escenarios opuestos:

  • Si tenemos un mini portal con unos pocos portlets, que no va a crecer, no se tiene pensado reutilizar, solo hay una implementación, etc. la solución 1 de portlet monolítico es perfecta, ¡no nos vamos a volver locos haciendo módulos y capas!
  • Si por el contrario tenemos un portal enorme, con decenas o cientos de portlets, un modelo de datos complejo, un entorno cambiante, necesitamos flexibilidad, a lo mejor diversas implementaciones de servicios (por ejemplo, una real y otra para entornos de test). En este caso necesitamos claramente la estrategia por capas.

La cuestión es como siempre, no hay que matar moscas a cañonazos. Si necesitamos implementar la opción 3 se hace, y una buena notación, separar en varios workspaces, no tenerlo todo desplegado, etc. Nos ayudará a implementarlo

La consola OSGi

La consola OSGi de Liferay es una potente herramienta a la que nos tenemos que acostumbrar dado que en Liferay todo son módulos y saber “qué ocurre por dentro” es vital.

Para acceder a la consola simplemente hay que hacer un telnet al puerto 11311.

Los comandos más utilizados son:

  • lb - List Bundles, saca una lista de todos los módulos. Notar que se pueden encadenar expresiones como “lb | grep foo”, pero también se puede hacer con “lb foo” directamente
  • b [nombre o id de bundle] - Da información de un módulo concreto.
  • scr:list – Muestra todos los componentes
  • scr:info - Muestra información de un componente
  • paclages – Muestra la lista de paquetes importados / exportados
  • start – Arranca un módulo
  • stop – Para un módulo
  • uninstall – Desinstala un módulo
  • help – muestra la ayuda
  • disconect – Desconecta la sesión. ¡Ojo! El comando exit para el contenedor OSGi Equinox.

Veamos un ejemplo básico de uso de la consola. Siguiendo los ejemplos que he hecho hasta el momento al ejecutar el comando “lb foo” tenemos:

g! lb foo  
START LEVEL 20  
   ID|State      |Level|Name
  507|Active     |    1|foo-web-all-gradleBundle (1.0.0)
  509|Active     |    1|foo-service (1.0.0)
  510|Active     |    1|foo-web-all-uber (1.0.0)
  511|Active     |    1|foo-web-serviceAll (0.0.0)
  512|Active     |    1|foo-infraestructure (1.0.0)
  513|Active     |    1|foo-web-all-compileInclude (1.0.0)
  514|Active     |    1|foo-web (1.0.0)
  515|Active     |    1|foo-api (1.0.0)
  516|Active     |    1|foo-common (1.0.0)
g!  

Si ahora queremos inspeccionar el bundle ‘foo-web’ podemos hacer “b foo-web” o bien “b 514”, y tenemos:

g! b 514  
foo-web_1.0.0 [514]  
  Id=514, Status=ACTIVE      Data Root=D:\L70_Platform\liferay-ce-portal-7.0-ga4\osgi\state\org.eclipse.osgi\514\data
  "Registered Services"
    {javax.portlet.Portlet}={javax.portlet.display-name=foo-web Portlet, component.name=org.atsistemas.foo.portlet.FooPortlet, com.liferay.portlet.instanceable=true, javax.portlet.security-role-ref=power-user,user, javax.portlet.init-par
am.view-template=/view.jsp, com.liferay.portlet.display-category=category.sample, javax.portlet.resource-bundle=content.Language, component.id=2453, javax.portlet.name=Foo, javax.portlet.init-param.template-path=/, service.id=6857, servi  
ce.bundleid=514, service.scope=bundle}  
    {org.osgi.service.http.context.ServletContextHelper}={osgi.http.whiteboard.context.name=foo-web, osgi.http.whiteboard.context.path=/foo-web, rtl.required=true, service.id=6858, service.bundleid=514, service.scope=singleton}
    {javax.servlet.ServletContextListener}={osgi.http.whiteboard.listener=true, osgi.http.whiteboard.context.select=(osgi.http.whiteboard.context.name=foo-web), service.id=6862, service.bundleid=514, service.scope=singleton}
    {javax.servlet.ServletContext}={osgi.web.contextpath=/o/foo-web, osgi.web.symbolicname=foo-web, osgi.web.version=1.0.0, osgi.web.contextname=foo-web, service.id=6863, service.bundleid=514, service.scope=singleton}
    {java.lang.Object}={osgi.http.whiteboard.resource.prefix=/META-INF/resources, osgi.http.whiteboard.context.select=(osgi.http.whiteboard.context.name=foo-web), osgi.http.whiteboard.resource.pattern=/*, service.id=6864, service.bundlei
d=514, service.scope=singleton}  
    {javax.servlet.Servlet}={osgi.http.whiteboard.servlet.pattern=[*.jsp,*.jspx], osgi.http.whiteboard.servlet.name=com.liferay.portal.osgi.web.servlet.context.helper.internal.JspServletWrapper, osgi.http.whiteboard.context.select=(osgi.
http.whiteboard.context.name=foo-web), service.id=6865, service.bundleid=514, service.scope=singleton}  
    {javax.servlet.Servlet}={osgi.http.whiteboard.servlet.pattern=/portlet-servlet/*, osgi.http.whiteboard.servlet.name=com.liferay.portal.kernel.servlet.PortletServlet, osgi.http.whiteboard.context.select=(osgi.http.whiteboard.context.n
ame=foo-web), service.id=6866, service.bundleid=514, service.scope=singleton}  
  Services in use:
    {org.atsistemas.foo.service.FooService}={component.name=org.atsistemas.foo.service.impl.FooServiceImpl, component.id=2450, service.id=6817, service.bundleid=509, service.scope=bundle}
    {org.osgi.service.log.LogService, org.eclipse.equinox.log.ExtendedLogService}={service.id=2, service.bundleid=0, service.scope=bundle}
    {com.liferay.portal.osgi.web.servlet.context.helper.ServletContextHelperRegistration}={service.id=4448, service.bundleid=11, service.scope=bundle}
    {org.osgi.service.http.context.ServletContextHelper}={osgi.http.whiteboard.context.name=foo-web, osgi.http.whiteboard.context.path=/foo-web, rtl.required=true, service.id=6858, service.bundleid=514, service.scope=singleton}
  No exported packages
  Imported packages
    com.liferay.portal.kernel.portlet.bridges.mvc; version="1.5.0" <org.eclipse.osgi_3.10.200.v20150831-0856 [0]>
    javax.portlet; version="2.0.0" <org.eclipse.osgi_3.10.200.v20150831-0856 [0]>
    javax.servlet; version="3.0.0" <org.eclipse.osgi_3.10.200.v20150831-0856 [0]>
    javax.servlet.http; version="3.0.0" <org.eclipse.osgi_3.10.200.v20150831-0856 [0]>
    org.atsistemas.foo.service; version="1.0.0" <foo.api_1.0.0 [515]>
    org.atsistemas.testhateoas.model; version="1.0.0" <foo.common_1.0.0 [516]>
  No fragment bundles
  No required bundles

g!  

Al pedir la información de un bundle básicamente tenemos:

  • Id: Identificador del bundle, típicamente un número
  • Status: Estado del bundle
  • Data Root: Lugar des de donde se ha cargado el bundle
  • Registered Services: Servicios que registra el bundle.
  • Services in use: Servicios que utiliza el bundle
  • Exported packages: Paquetes marcados como exportados.
  • Imported packages: Paquetes importados

Vamos a analizar el caso concreto del bundle solo entrando en los
detalles más significativos:

  • Id: 514
  • Status: ACTIVE
  • Data Root: D:\L70_Platform\liferay-ce-portal-7.0-ga4\osgi\state\org.eclipse.osgi\514\data
Notar que el directorio “[LIFERAY_HOME]\osgi\state\org.eclipse.osgi\” es donde se almacenan los bundles desplegados. Para desinstalar un bundle podermos haver “uninstall [id]” en la consola o bien directamente borrar el bundle del sistema de ficheros.
  • Registered Services: Entre varios servicios que registra tenemos:
{javax.portlet.Portlet}={javax.portlet.display-name=foo-web Portlet,component.name=org.atsistemas.foo.portlet.FooPortlet,com.liferay.portlet.instanceable=true,javax.portlet.security-role-ref=power-user,user,javax.portlet.init-param.view-template=/view.jsp,com.liferay.portlet.display-category=category.sample,javax.portlet.resource-bundle=content.Language, component.id=2453,javax.portlet.name=Foo, javax.portlet.init-param.template-path=/,service.id=6857, service.bundleid=514, service.scope=bundle}
  • Que es precisamente lo que ocurre cuando anotamos la clase portlet con:
@Component(
immediate = true,  
..
service = Portlet.class  
  • Services in use: Entre los varios servicios que utiliza podemos ver que se está utilizando la implementación del servicio FooService:
{org.atsistemas.foo.service.FooService}={component.name=org.atsistemas.foo.service.impl.FooServiceImpl, component.id=2450, service.id=6817, service.bundleid=509, service.scope=bundle}
Notar que en este punto da mucha información de la inyección como es la interficie destino de la inyección, la clase concreta que se carga, el id del componente, el id de servicio, y el bundle des de donde se ha cargado la instancia de la clase que se inyecta. Esto puede parecer trivial cuando estamos hablando de un servicio desarrollado por nosotros mismos y donde solo hay una implementación, pero, en varias ocasiones puede ocurrir que no se esté inyectando lo que esperábamos. (como veremos más adelante ;) ).
  • Exported packages: En este caso no se exportan paquetes.
  • Imported packages: Se importan los paquetes relacionados con Portlets, Servlets y los relativos a foo-common y foo-service (¿recordamos las flechitas azules del esquema general?).

Pequeños trucos

¿De dónde se carga una clase?

En muchas ocasiones podemos dudar en cuanto a de donde se carga una clase, para ello, estaría bien tener un procedimiento averiguarlo. En este caso tenemos dos opciones:
- Mediante la consola OSGI. Como hemos visto anteriormene es fácil el consultar de donde se ha cargado una clase mediante el comando “budle”:

{org.atsistemas.foo.service.FooService}={component.name=org.atsistemas.foo.service.impl.FooServiceImpl, component.id=2450, service.id=6817, service.bundleid=509, service.scope=bundle}

Lo cual indica que la clase FooServiceImpl se ha cargado a partir del bundle 509, que se ubica en la carpeta [LIFERAY_HOME]\osgi\state\org.eclipse.osgi\509.

  • Por código. Mediante el API de Java podemos saber de dónde se ha cargado una clase. Una “sencilla” instrucción como la siguiente nos puede ayudar:

String sSource = this.getClass().getClassLoader().loadClass("org.atsistemas.testhateoas.model.Foo").getProtectionDomain().getCodeSource().getLocation().toString();

Esta instrucción retornaría la ubicación del jar bundle que contiene
la clase que hemos pedido:>

file:/D:/L70_Platform/liferay-ce-portal-7.0-ga4/osgi/state/org.eclipse.osgi/505/0/bundleFile

Errores más comunes

En este apartado comentaré los errores más típicos que se suelen dar trabajando con módulos en Liferay y que seguramente ahorraran más de un dolor de cabeza a algún lector :P.

Unresolved requirement: Import-Package:

Este es el error más típico y ocurre cuando tenemos una dependencia en tiempo de compilación, pero esta no se puede encontrar en tiempo de ejecución.

Otro caso muy común es cuando en el momento de la construcción del bundle se analizan los imports de todas las clases referenciadas y se construye la sentencia Import-Package del Manifest.mf. Que haya un Import en una clase no quiere decir que se utilice, simplemente puede ser un descuido, un complemento que no queramos, o que simplemente, por el flujo de ejecución del código no se vaya a necesitar tener esa clase en el Classpath de ejecución. Lo que típicamente en Java fallaría en tiempo de ejecución con un

Lo que simplemente en tiempo de ejecución daría un ClassNotFoundException, en OSGi falla en tiempo de despliegue con el mensaje “Unresolved requirement”.

Pongamos un ejemplo concreto. Al empaquetar las librerías de Spring en OSGi si en bnd.bnd utilizamos la directiva “Import-Package: *” al instalar el módulo en OSGi da el siguiente error:

BUILD SUCCESSFUL  
Total time: 9.121 secs  
stop 513  
update 513  
file:/D:/L70\_Platform/workspaces/liferay/testHateoas/modules/foo-web-all-compileInclude/build/libs/foo.web.all.compileInclude-1.0.0.jar  
start 513  
org.osgi.framework.BundleException: Could not resolve module:  
foo.web.all.compileInclude \[513\]  
Unresolved requirement: Import-Package: bsh  
Updated bundle 513  

La primera impresión es “¿bsh es un paquete?” y la segunda “¿Y se utiliza dentro de Spring?”. Pues nada, cogemos el código de Spring y hacemos un ‘find in files’, y sí, resulta que sí:

----------------------------------------
Find 'import bsh' in 'Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptEvaluator.java' (18/01/2018 17:35:19; 10/10/2017 12:34:30):  
Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptEvaluator.java(23): import bsh.EvalError;  
Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptEvaluator.java(24): import bsh.Interpreter;  
Found 'import bsh' 2 time(s).  
----------------------------------------
Find 'import bsh' in 'Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptFactory.java' (18/01/2018 17:35:19; 10/10/2017 12:34:30):  
Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptFactory.java(21): import bsh.EvalError;  
Found 'import bsh' 1 time(s).  
----------------------------------------
Find 'import bsh' in 'Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptUtils.java' (18/01/2018 17:35:19; 10/10/2017 12:34:30):  
Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptUtils.java(23): import bsh.EvalError;  
Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptUtils.java(24): import bsh.Interpreter;  
Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptUtils.java(25): import bsh.Primitive;  
Z:\Spring\spring-context-4.3.12.RELEASE-sources\org\springframework\scripting\bsh\BshScriptUtils.java(26): import bsh.XThis;  
Found 'import bsh' 4 time(s).  
Search complete, found 'import bsh' 7 time(s). (3 file(s)).  

Ante esto podemos utilizar el operador negativo de OSGi, la exclamación (!), y en la directiva bnd poner:

Import-Package: !bsh,*

Ahora, habiendo realizado ese cambio volvemos a desplegar y tenemos:

Unresolved requirement: Import-Package: com.caucho.burlap.client  

¡Vaya! Otro más, que lata. Pues nada, otro import negativo y ya está, que faltaran pocos… Ponemos en el bnd:

Import-Package: !bsh,!com.caucho.burlap.client,*

Y al volver a desplegar tenemos:

Unresolved requirement: Import-Package: com.caucho.burlap.io

La cuestión es que no faltan pocos, hay muchos, tantos que no podemos abordarlos así.

Otra opción de la importación es “resolution:=optional”, que indica precisamente eso, que si no encuentra un paquete no falle en el despliegue (posiblemente será un package que no se utilice en tiempo de ejecución). Esta opción se puede aplicar a un import concreto o bien al import general de *. Es por este motivo que en varios ejemplos se ha utilizado la sentencia: “Import-Package: *;resolution:=optional”.

Añadir dependencias dinámicas

OSGi debe tener toda la información necesaria para poder ejecutar un módulo, y, dentro de ejecución está el montar el classpath. Cuando montamos el módulo OSGi e indicamos en bnd “Import-Package: *;” básicamente se analizan los imports de todas las clases referenciadas por el módulo y se añaden como imports al descriptor OSGi si es que estos no son paquetes internos. Pero claro, esto funciona para las referencias estáticas, los imports de las clases, si por casualidad en alguna clase tenemos una instanciación dinámica por Reflection esta no la encontrará bnd y no se añadirá como import en el MANIFEST.MF. En estos casos es necesario el añadirla de forma explícita en OSGi.

Por ejemplo hacemos “loadClass” de una clase de Liferay (p.e. MBMessageLocalServiceUtil):

Class clClazz = **this**.getClass().getClassLoader().  
loadClass("com.liferay.message.boards.kernel.service.MBMessageLocalServiceUtil");  

Al desplegar no fallará, pero en tiempo de ejecución evidentemente tendremos:

java.lang.ClassNotFoundException:  
com.liferay.message.boards.kernel.service.MBMessageLocalServiceUtil  
cannot be found by foo.web.all.compileInclude\_1.0.0  

Para ello, es necesario poner de forma explícita la importación en bnd.bnd:

Import-Package:  
com.liferay.message.boards.kernel.service,\*;resolution:=optional  

En este punto cabe comentar que bnd también analiza las JSP, por tanto, los imports que se encuentren en ellas también los añadirá. Por ejemplo, si llamamos a la clase MBMessageLocalServiceUtil en el fichero view.jsp sin ser esta clase utilizada en el código Java ‘directo’, esta se encontrará correctamente

<%@page import=*"com.liferay.message.boards.kernel.model.MBMessage"*>  
<%@page import=*"com.liferay.message.boards.kernel.service.MBMessageLocalServiceUtil"*>  
<% MBMessage mbMessage = MBMessageLocalServiceUtil.getMessage(1l);>  
<%= mbMessage >  

Si te ha gustado el artículo, ¡síguenos en Twitter para estar al día de nuevos posts!