Configuración Equinox, OSGi, Spring DM y Maven

Actualmente estoy trabajando en un proyecto en el que OSGi va a jugar un papel determinante. En próximos posts hablaré sobre OSGi, y sobre que papel juega dentro de las aplicaciones Java.

Cuando se habla de OSGi, se habla de programación modular donde un módulo es un fichero JAR (Java ARchive). En OSGi un módulo tiene el nombre de bundle.

Para poder entender este post se requieren conocimientos de :

  • Maven 2
  • Spring Framework
  • Eclipse, Equinox
  • OSGi
  • Spring Dynamic Modules (Spring DM)

La prueba de concepto que vamos a describir a continuación, paso a paso, es la siguiente:

  • Crear un bundle e instalarlo en Equinox.
  • Asociar al bundle una clase BundleActivator para comprobar que el bundle ha sido inicializado correctamente.
  • Instalar el bundle Extender de Spring DM y sus dependencias dentro de Equinox.
  • Configurar el bundle para que arranque un ApplicationContext de Spring.

La idea de este post es mostrar de manera rápida el proceso de configuración, por ello no voy a explicar detalladamente el conjunto de herramientas y plugins utilizados a lo largo del proceso. Esto quedará pendiente para próximos posts.

1. Creación de un bundle OSGi

Creamos un nuevo proyecto Java dentro de nuestro espacio de trabajo de Eclipse. En mi caso, es.jmac.springdm

Mavenizamos ese proyecto, yo he usado el plugin de Sonatype M2Eclipse, pero vamos puede hacerse a mano, basta crear un fichero pom.xml.

La información del fichero Maven de configuración del proyecto será la siguiente :

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>es.jmac</groupId>
  <artifactId>springdm</artifactId>
  <packaging>jar</packaging>
  <name>springdm</name>
  <version>1.0.0-SNAPSHOT</version>
  <description>Prueba de concepto de Spring Dynamic Modules sobre Equinox</description>

</project>

Creamos una nueva source folder, src/main/java.

Al final del proceso, nuestro proyecto en Eclipse debería tener una estructura similar a la siguiente:

Estructura inicial del proyecto

Estructura inicial del proyecto

Una vez que tenemos nuestro proyecto Java mavenizado vamos a incorporarle la información necesaria que lo identifique como un bundle OSGi.

Al inicio del post se ha comentado que un bundle OSGi es un fichero JAR. Sin embargo no basta con eso. Ese fichero JAR debe contener un información concreta que permita a la plataforma OSGi identificar ese fichero como un bundle OSGi. Esta información viene especificada en el fichero MANIFEST.MF. Esa será nuestra siguiente tarea generar ese fichero con la información necesaria para ser identificado como un bundle OSGi.

Bundle OSGi = JAR + MANIFEST.MF con CABECERAS OSGi

Bueno, ahora hay que generar el MANIFEST.MF. Hay varias formas de hacerlo, se puede hacer mano, si se conocen el conjunto de cabeceras OSGi o bien a través de alguna herramienta. En mi caso he utilizado un plugin para Maven que permite realizar varias operaciones sobre bundles OSGI, maven-bundle-plugin. Como he comentado anteriormente, no voy a explicar como funciona el plugin, os indico que hay que poner en el pom.xml y la orden que hay que lanzar.

Actualizar el pom.xml con el siguiente código :

...
<packaging>bundle</packaging>
...
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.felix</groupId>
      <artifactId>maven-bundle-plugin</artifactId>
      <version>2.0.0</version>
      <extensions>true</extensions>
      <configuration>
        <instructions>
	  <Bundle-SymbolicName>${groupId}.${artifactId}</Bundle-SymbolicName>
	  <Bundle-Version>${pom.version}</Bundle-Version>
	</instructions>
      </configuration>
    </plugin>
  </plugins>
</build>
...

ATENCIÓN : No olvidéis cambiar el valor de elemento packaging a bundle.

Ejecutad por consola o a través de Eclipse, el siguiente comando Maven :


mvn clean package -Declipse.pde

Esta orden generará un fichero JAR con nuestro proyecto, que por ahora no tiene nada, y que contendrá un fichero MANIFEST.MF con las cabeceras necesarias para ser reconocido como un bundle OSGi. Lo que tenéis que hacer es iros a la carpeta target donde se ha copiado el fichero JAR y sacar de él fichero MANIFEST.MF para copiarlo a vuestro proyecto en Eclipse, ojo, dentro de la carpeta META-INF. Ya sé que no es muy sutil, pero es rápido.

ACTUALIZACIÓN
Una forma más automática de generar el fichero MANIFEST.MF sería utilizando el goal manifest del anterior plugin. Tendríamos que cambiar nuestra definición anterior del plugin por la siguiente:

...
<plugin>
  <groupId>org.apache.felix</groupId>
  <artifactId>maven-bundle-plugin</artifactId>
  <version>2.0.0</version>
  <extensions>true</extensions>
  <configuration>
    <manifestLocation>META-INF</manifestLocation>
    <instructions>
      <Bundle-SymbolicName>${groupId}.${artifactId}</Bundle-SymbolicName>
      <Bundle-Version>${pom.version}</Bundle-Version>
    </instructions>
  </configuration>
  <executions>
    <execution>
      <id>genera-manifest</id>
      <phase>generate-resources</phase>
      <goals>
        <goal>manifest</goal>
      </goals>
    </execution>
    <execution>
      <id>genera-bundle</id>
      <phase>package</phase>
      <goals>
        <goal>bundle</goal>
      </goals>
    </execution>
  </executions>
</plugin>
...

El plugin anterior tiene dos ejecuciones en dos fases distintas del ciclo de vida de Maven. En la primera fase, generate-resources, se ejecuta el goal manifest. En la segunda fase, package, se ejecuta el goal definido por defecto en el plugin, bundle.

No olvidar añadir el elemento :

<manifestLocation />

Vuestro fichero MANIFEST.MF debería contener algo parecido a esto:

Manifest-Version: 1.0
Built-By: justo
Created-By: Apache Maven Bundle Plugin
Import-Package: es.jmac.springdm,org.osgi.framework;version="1.3"
Export-Package: es.jmac.springdm;uses:="org.osgi.framework"
Bundle-Version: 1.0.0.SNAPSHOT
Bundle-Name: springdm
Bundle-Description: Prueba de concepto de Spring Dynamic Modules sobre
 Equinox
Build-Jdk: 1.5.0_19
Private-Package: es.jmac.springdm,
Bundle-ManifestVersion: 2
Bundle-SymbolicName: es.jmac.springdm
Tool: Bnd-0.0.311
2. Ejecución del bundle dentro de Equinox

Ya hemos creado un bundle OSGi, ahora vamos a ver si lo hemos creado correctamente. Para ello, vamos a lanzar una instancia de Equinox y especificaremos que dentro de esa instancia se cargue nuestro bundle recien creado.

Nos vamos al menún Run –> Run Configurations… de Eclipse

Run Configurations...

Run Configurations...

Selecciónamos OSGi Framework y pulsando el botón derecho seleccionamos New…

New OSGi Framework

New OSGi Framework

En la parte derecha de la ventana aparecerán la lista de bundles que tenemos actualmente, dividos en dos grupos, los bundles creados en nuestro workspace y los bundles instalados en la Target Platform, que suele ser $ECLIPSE_HOME/plugins.

Lista de Bundles

Lista de Bundles

Los siguientes pasos serán seleccionar nuestro bundle, deseleccionar todos los bundles de la sección Target Platform y pichar sobre el botón Add Required Bundles, es nos marcará un único bundle el core de OSGi, org.eclipse.osgi. Pinchamos en Apply para salvar los cambios y pichamos en Run.

A continuación se debe abrir la ventana de Console en la que aparecerá el shell de OSGi, algo como :
osgi>
Para comprobar si nuestro bundle ha sido inicializado correctamente, introducimos el comando ss y pulsamos intro. Debería aparecer lo siguiente, si no, algo hemos mal :

Short Status (ss)

Short Status (ss)

Acabamos de ejecutar nuestro bundle dentro de Equinox. Tampoco tiene mucho mérito porque es un bundle vacío, pero bueno, vamos poco a poco.

3. Añadimos una clase BundleActivator a nuestro bundle

Una clase activator se encarga de inicializar/liberar un bundle. Esta pequeña clase nos avisará que el bundle ha sido inicializado/parado correctamente.

La clase debe implementar la interfaz BundleActivator definida en la API de OSGi. Para que nuestro código compile correctamente será necesario añadir un par de dependencias a nuestro pom.xml. Basta con añadir el siguiente fragmento de código:

<dependencies>
  ...
  <dependency>
    <groupId>org.osgi</groupId>
    <artifactId>osgi_R4_core</artifactId>
    <version>1.0</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>org.osgi</groupId>
    <artifactId>osgi_R4_compendium</artifactId>
    <version>1.0</version>
    <scope>provided</scope>
  </dependency>
...
</dependencies>

El código de la clase SpringDMActivator es el siguiente:

package es.jmac.springdm;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

/**
 * @author Justo Aguilar
 * Creation date : 20/07/2009
 *
 * <p>
 */
public class SpringDMActivator implements BundleActivator {

	public void start(BundleContext context) throws Exception {
		System.out.println("Arrancando bundle " + context.getBundle().getSymbolicName() + "...");
	}

	public void stop(BundleContext context) throws Exception {
		System.out.println("Parando bundle " + context.getBundle().getSymbolicName() + "...");
	}
}

Un último paso necesario es añadir al fichero MANIFEST.MF la siguiente línea:

Bundle-Activator: es.jmac.springdm.SpringDMActivator

con la ruta de nuestro clase BundleActivator.

La próxima vez que arranquemos la instancia del OSGi Framework creada anteriormente debería aparecer el mensaje definido por el método start de la clase SpringDMActivator.

4. Spring DM entra en juego

En este post se hace un uso trivial e insignificante de Spring DM. En próximos post se mostrará la verdadera potencia de integrar Spring Framework con OSGi.

Nuestro objetivo en este último paso es inicializar un ApplicationContext de Spring al iniciar un bundle en OSGi.

Lo primero que haremos será crear un POJO para referenciarlo posteriormente en el fichero applicationContext.xml de Spring. Yo he creado una clase Entity :

package es.jmac.springdm.domain;

import java.io.Serializable;

/**
 * @author Justo Aguilar
 * Creation date : 20/07/2009
 *
 */
public class Entity implements Serializable {

	/** Identificador de Serialización  */
	private static final long serialVersionUID = 147003001012587224L;

	/** Identificador de la entidad */
	private Long id;

	/** Nombre de la entidad */
	private String name;

	/**
	 * Constructor de Entity. Necesita el identificador.
	 * @param id Identificador de la entidad.
	 */
	public Entity(Long id) {
		this.id = id;
		System.out.println("Creando entidad con id : " + id);
	}

}

A continuación hay que crear y definir el fichero applicationContext.xml de Spring. Este fichero se situará dentro de una carpeta spring que a su vez debe estar contenida en la carpeta META-INF del proyecto.

Proyecto
|__META-INF
    |__spring
        |__applicationContext.xml

Este fichero contendrá la siguiente información :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="entity" class="es.jmac.springdm.domain.Entity">
      <constructor-arg><value>1</value></constructor-arg>
    </bean>
</beans>

Ahora es cuando entra en juego Spring DM. Evidentemente, por si solo, Equinox no tiene ni idea para que sirve la carpeta spring creada bajo META-INF ni los ficheros contiene ésta, por tanto si volvemos a arrancar Equinox veremos que no sucede nada nuevo. Para dar un tratamiento especial a este tipo de bundles, Spring-Powered bundle según la documentación de Spring DM, es necesario que alguién esté pendiente de cuando se inicializa o se para este tipo de bundles. Ese alguien es conocido como Extender. Extender no es más que un bundle que está a la escucha del ciclo de vida del restos de bundles, cuando un bundle se inicializa o se para, comprueba si se trata de un Spring-Powered bundle para realizar la tarea de inicialiar/parar el ApplicationContext asociado al éste.

Por tanto será necesario instalar el bundle Extender y todos sus dependencias dentro de la Target Platform y marcarlos para que se ejecuten dentro de Equinox. Bueno, vamos por pasos.

Primero, nos descargamos todos los bundles que necesitamos. Para ello acudimos al repositorio de springsource, http://www.springsource.com/repository/app/ y nos descargamos el jar org.springframework.osgi.extender, en mi caso la versión 1.2.0. En la parte inferior de esa ventana aparece una sección plegada que dice Required Dependencies (9), la desplegamos y nos bajamos cada uno de los bundles que ahí se indican.

Dependencias Bundle Extender de Spring DM

Dependencias Bundle Extender de Spring DM

Segundo, copiamos los JARs descargados al directorio de nuestra Target Platform, si no sabes a lo que me refiero, no lo habrás cambiado asi que el directorio es $INSTALACION_ECLIPSE/plugins. Reiniciamos Eclipse.

Tercero, abrimos la ventana Run Configurations… y comprobamos que entre los bundles de la sección Target Platform se encuentran los que acabamos de copiar. Marcamos el bundle de Extender y volvemos a pinchar en Add Required Bundles, veremos como se seleccionan el resto de bundles copiados en el paso anterior. Aplicamos y ejecutamos.

Si no os aparece el mensaje definido en el constructor del POJO, algo habréis hecho mal.

Javadoc con Diagramas de Clases usando Graphviz + UMLGraph + Maven

Una de las herramientas más importantes con las que cuenta la plataforma Java es, sin duda, el javadoc. A través de ella es posible crear una referencia completa y homogénea de todo el conjunto de clases definidas es nuestro proyecto.

Toda página de una clase del javadoc está dividida en distintas zonas, la primera de ellas muestra la situación jerárquica de la clase respecto a la clase padre Object. Bajo la situación jerárquica aparece la lista de interfaces que implementa si existe alguna y, por último, la lista de clases que la extienden. Para las interfaces aparece la lista de superinterfaz en primer lugar y la lista de subinterfaces en segundo.

Aunque esta información pueda ser suficiente en muchos casos, no estaría de más completarla con diagramas de clases que permitan una rápida visualización de la jerarquía de clases de nuestro proyecto. Ese es el objetivo de este post, añadir diagramas de clases UML a nuestro javadoc.

Diagrama UML generado con UMLGraph

Para ello necesitaremos dos cosas básicamente :

  • Graphviz, librería que permite la generación de gráficos.
  • UMLGraph plugin para maven que sobreescribe el doclet que genera el javadoc para insertar los gráficos generados por Graphviz.
  • Maven

A continuación vamos a ver como instalar Graphviz y como configurar el pom.xml del proyecto maven para que use el plugin UMLGraph.

Instalación de Graphviz

Hay varias formas de hacerlo, bajando el fuente y compilando, haciendo un checkout del repositorio CVS o dejando que el sistema de paquetes de tu sistema operativo lo haga todo por tí ;-). Esto último es lo que haremos pues la finalidad de éste post es otra.

sudo apt-get update
sudo apt-get install graphviz
man dot

La última entrada es sólo para comprobar que el paquete ha sido instalado correctamente.

Modificación del fichero pom.xml del proyecto

Simplemente se debe añadir al fichero las siguientes líneas de código

...
<reporting>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-javadoc-plugin</artifactId>
      <configuration>
        <doclet>gr.spinellis.umlgraph.doclet.UmlGraphDoc</doclet>
        <docletArtifact>
          <groupId>gr.spinellis</groupId>
          <artifactId>UmlGraph</artifactId>
          <version>4.6</version>
        </docletArtifact>
        <additionalparam>
          -inferrel -inferdep -quiet -hide java.* -collpackages java.util.* -qualify
          -postfixpackage -nodefontsize 9
          -nodefontpackagesize 7
        </additionalparam>
      </configuration>
    </plugin>
  </plugins>
</reporting>
...

Para generar el javadoc se ejecutará el comando mvn javadoc:javadoc dentro del directorio
donde se encuentra el fichero pom.xml.

Javadoc de la clase Nivel 1

Configurar proxy en Subversion y Maven

Hoy he empezado a trabajar en las oficinas del cliente del proyecto que me ocupará los próximos meses y he tenido que configurar todo mi sistema para que se conecte a Internet a través de un proxy. Aunque esta configuración es trivial, voy a comentar los ficheros que hay modificar para que tanto Maven como Subversion pueden acceder a repositorios externos a través de un proxy.

Maven

En Maven el fichero que tiene que ser modificado se llama settings.xml y está situado bajo el directorio $MAVEN_HOME/config, siendo $MAVEN_HOME el directorio base de instalación de Maven.

Dentro de este fichero será necesario buscar el siguiente bloque de código :

<proxies>
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net,some.host.com</nonProxyHosts>
</proxy>
</proxies>

Simplemente debéis establecer los parámetros de la configuración de acceso al proxy en los elementos correspondientes, por ejemplo :

<proxy>
<!-- Parámetro opcional -->
<id>Proxy oficina cliente</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyUser</username>
<password>proxyPassword</password>
<host>10.23.22.43</host>
<port>80</port>
<!-- Direcciones a excluir de pasar por el proxy -->
<nonProxyHosts>localhost, 10.23.23.44</nonProxyHosts>
</proxy>
</proxies>

Subversion

La configuración de Subversion es muy parecida, aunque permite mayor flexibilidad. En este caso el fichero a modificar se encuentra en la ruta /etc/subversion/servers. La flexibilidad que aporta Subversion a la hora de configurar el acceso a un repositorio via proxy está reflejada en la posibilidad de configurar un proxy global para todos los repositorios o la posibilidad de establecer la configuración de un proxy para grupo de repositorios.

El ficheros de configuración base es el siguiente :

### This file specifies server-specific protocol parameters,
### including HTTP proxy information, and HTTP timeout settings.
###
### The currently defined server options are:
###   http-proxy-host            Proxy host for HTTP connection
###   http-proxy-port            Port number of proxy host service
###   http-proxy-username        Username for auth to proxy service
###   http-proxy-password        Password for auth to proxy service
###   http-proxy-exceptions      List of sites that do not use proxy
###   http-timeout               Timeout for HTTP requests in seconds
###   http-compression           Whether to compress HTTP requests
###   neon-debug-mask            Debug mask for Neon HTTP library
###   ssl-authority-files        List of files, each of a trusted CAs
###   ssl-trust-default-ca       Trust the system 'default' CAs
###   ssl-client-cert-file       PKCS#12 format client certificate file
###   ssl-client-cert-password   Client Key password, if needed.
###
### HTTP timeouts, if given, are specified in seconds.  A timeout
### of 0, i.e. zero, causes a builtin default to be used.
###
### The commented-out examples below are intended only to
### demonstrate how to use this file; any resemblance to actual
### servers, living or dead, is entirely coincidental.

### In this section, the URL of the repository you're trying to
### access is matched against the patterns on the right.  If a
### match is found, the server info is from the section with the
### corresponding name.

[groups]
# group1 = *.collab.net
# othergroup = repository.blarggitywhoomph.com
# thirdgroup = *.example.com

### Information for the first group:
# [group1]
# http-proxy-host = proxy1.some-domain-name.com
# http-proxy-port = 80
# http-proxy-username = blah
# http-proxy-password = doubleblah
# http-timeout = 60
# neon-debug-mask = 130

### Information for the second group:
# [othergroup]
# http-proxy-host = proxy2.some-domain-name.com
# http-proxy-port = 9000
# No username and password, so use the defaults below.

### You can set default parameters in the 'global' section.
### These parameters apply if no corresponding parameter is set in
### a specifically matched group as shown above.  Thus, if you go
### through the same proxy server to reach every site on the
### Internet, you probably just want to put that server's
### information in the 'global' section and not bother with
### 'groups' or any other sections.
###
### If you go through a proxy for all but a few sites, you can
### list those exceptions under 'http-proxy-exceptions'.  This only
### overrides defaults, not explicitly matched server names.
###
### 'ssl-authority-files' is a semicolon-delimited list of files,
[global]
# http-proxy-exceptions = *.exception.com, www.internal-site.org
# http-proxy-host = defaultproxy.whatever.com
# http-proxy-port = 7000
# http-proxy-username = defaultusername
# http-proxy-password = defaultpassword
# http-compression = no
# No http-timeout, so just use the builtin default.
# No neon-debug-mask, so neon debugging is disabled.
# ssl-authority-files = /path/to/CAcert.pem;/path/to/CAcert2.pem

Como puedes observar, en la primera parte del fichero se definen los grupos, groups, con la url de los repositorios asociados.

[groups]
group1 = *.collab.net
othergroup = repository.blarggitywhoomph.com
thirdgroup = *.example.com

Observad que se permite el mapeo de todos los subdominios de un dominio mediante el uso de *.

En la segunda parte del fichero, se especifica la configuración del proxy asociada a cada grupo. En primer lugar se especifica, [entre corchetes] el nombre del grupo de repositorios a configurar y posteriormente los parámetros propios de configuración del proxy, cuyos nombres son autoexplicativos.

[group1]
http-proxy-host = proxy1.some-domain-name.com
http-proxy-port = 80
http-proxy-username = blah
http-proxy-password = doubleblah
http-timeout = 60

Por último, es posible definir una configuración global de acceso a todos los repositorios, se realiza bajo la etiqueta [global]

[global]
http-proxy-exceptions = localhost, 10.23.23.44
http-proxy-host = 10.23.22.43
http-proxy-port = 80
http-proxy-username = proxyUser
http-proxy-password = proxyPassword

Para una configuración más avanzada consultar la referencia oficial.