Modularizzare Java con JBoss V parte: Esempi con OSGi

V parte: Esempi con OSGidi

In questo articolo continuiamo la trattazione di OSGi, e in particolare vediamo alcuni esempi di implementazione di alcuni servizi dell'OSGi service compendium e qualche esempio di integrazione OSGi-EJB.

Introduzione

Le specifiche OSGi definiscono un ambiente di servizi modulari che permettono di implementare servizi in maniera standardizzata, seguendo l'approccio component-oriented.

Si tratta dunque di uno standard che permette di implementare dei servizi definiti mediante dei moduli che espongono solamente il codice inerente il contratto da implementare.

Accanto allo standard OSGi (OSGi Core Framework), nel precedente articolo abbiamo visto come sia stato definito un secondo standard che descrive dei servizi aggiuntivi che potrebbero essere offerti da un runtime environment. Essi formano l'OSGi Compendium Service.

Il Compendium definisce dei servizi estremamente utili che qualunque sviluppatore di bundle OSGi si potrebbe/vorrebbe implementare "in casa": ecco quindi che il Compendium permette di avere questi servizi già pronti senza doverli reimplementare ogni volta.

In questo articolo forniamo degli esempi di utilizzo di alcuni servizi del Compendium e anche degli esempi di integrazione con CDI/EJB. Iniziamo ora a vedere il Blueprint Container, il Declarative Services e il JNDI Service. Infine vedremo un esempio di utilizzo di un servizio OSGi all'interno di un Session Bean.

Blueprint Container

Il Blueprint Container permette di definire dei servizi OSGi e di legarli assieme via IoC "alla Spring" senza avere a che fare con le interfacce OSGi.

Supponiamo di dover definire due servizi, A e B, ognuno dei quali ha al suo interno dei "backed bean" BeanA e BeanB. BeanB ha al suo interno un riferimento all'istanza di BeanA. Per definire i due servizi e i due bean, utilizziamo un file XML di configurazione molto simile a quelli utilizzati da Spring:

           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0
http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
 
  ="org.jboss.test.osgi.example.blueprint.bundle.BeanA" init-method="init">
 
   
 
 

 
  interface="org.jboss.test.osgi.example.blueprint.bundle.ServiceA">
 
 

 
 
 
 
   
 
     
 
   

 
 

 
 
 

I servizi A e B sono definiti come segue:

public interface ServiceB {
 
  BeanA getBeanA();
 
}

Il servizio A fornisce un riferimento al BeanA, mentre il servizio B ritorna solamente il riferimento al MbeanServer:

public interface ServiceA {
 
  MBeanServer getMbeanServer();
 
}

BeanA e BeanB sono dei semplici POJO che contengono solamente il metodo di inizializzazione,  le proprietà che devono essere valorizzate dal contenitore e i rispettivi getter/setter:

public class BeanA implements ServiceA {
 
  private MBeanServer mbeanServer;
 
  public BeanA() { }
 
  public void init() { }
 
  public MBeanServer getMbeanServer() {
 
    return mbeanServer;
 
  }
 
  public void setMbeanServer(MBeanServer mbeanServer) {
 
    this.mbeanServer = mbeanServer;
 
  }
 
}
 
 
 
public class BeanB implements ServiceB {
 
  private BeanA beanA;
 
  public BeanB() { }
 
  public void init() { }
 
  public BeanA getBeanA() {
 
    return beanA;
 
  }
 
  public void setBeanA(BeanA beanA) {
 
      this.beanA = beanA;
 
  }
 
}

Come si vede, non si fa mai riferimento al BundleActivator n��� tanto meno al ciclo di vita di un bean OSGi.

Test con Arquillan

Costruiamo ora il bundle con i due servizi e testiamolo con Arquillan. Prima di tutto creiamo il TestCase per il Blueprint:

@RunWith(Arquillian.class)
 
public class BlueprintTestCase {
 

 
  @ArquillianResource
 
  Deployer deployer;
 
  @ArquillianResource
 
  BundleContext context;
 

 
}

In questo modo abbiamo creato un test case JUnit che utilizza Arquillan (@RunWith), le risorse deployer e context vengono valorizzate da Arquillan e sono rispettivamente il servizio di deployment del bundle e il contesto OSGi. Adesso creiamo il bundle OSGi da deployare:

  @Deployment
 
  public static JavaArchive blueprintProvider() {
 
    final JavaArchive archive
      = ShrinkWrap.create(JavaArchive.class, "blueprint-tests");
 
    archive.addClasses(ProvisionerSupport.class, BlueprintSupport.class);
 
    archive.addClasses(BeanA.class, ServiceA.class,
                       BeanB.class, ServiceB.class);
 
    archive.addAsResource("repository/aries.blueprint.feature.xml");
 
    archive.addAsResource("repository/jbosgi.jmx.feature.xml");
 
    archive.setManifest(new Asset() {
 
      @Override
 
      public InputStream openStream() {
 
        OSGiManifestBuilder builder = OSGiManifestBuilder.newInstance();
 
        builder.addBundleSymbolicName(archive.getName());
 
        builder.addBundleManifestVersion(2);
 
        builder.addImportPackages(XRepository.class, Repository.class,
                                  XResource.class, Resource.class,
                                  XResourceProvisioner.class);
 
        builder.addDynamicImportPackages(BlueprintContainer.class,
                                         MBeanServer.class);
 
        builder.addImportPackages(ServiceTracker.class);
 
        builder.addExportPackages(ServiceA.class);
 
        return builder.openStream();
 
      }
 
    });
 
    return archive;
 
  }

Come si vede si crea un archivio con le nostre classi (ServiceA, ServiceB, BeanA e BeanB), si rendono disponibili le feature Blueprint (aries.blueprint.feature.xml) e JMX (jbosgi.jmx.feature.xml) mediante due file XML che elencano molte capabilities (sono repository di capabilities), tra cui quelle per il servizio Blueprint e JMX.

Un ulteriore bundle

Adesso definiamo un altro bundle da installare: questo bundle contiene i servizi Blueprint e JMX che ci servono per poter attivare il bundle che abbiamo appena definito:

  @Deployment(name = BLUEPRINT_BUNDLE, managed = false, testable = false)
 
  public static JavaArchive testBundle() {
 
    final JavaArchive archive
      = ShrinkWrap.create(JavaArchive.class, BLUEPRINT_BUNDLE);
 
    archive.addAsResource("blueprint/blueprint-example.xml",
                          "OSGI-INF/blueprint/blueprint-example.xml");
 
    archive.setManifest(new Asset() {
 
      @Override
 
      public InputStream openStream() {
 
        OSGiManifestBuilder builder = OSGiManifestBuilder.newInstance();
 
        builder.addBundleSymbolicName(archive.getName());
 
        builder.addBundleManifestVersion(2);
 
        builder.addImportPackages("org.osgi.service.blueprint;
                                  version=‘[1.0.0,2.0.0)'");
 
        builder.addImportPackages(BlueprintContainer.class,
                                  MBeanServer.class);
 
        builder.addImportPackages(ServiceA.class);
 
        return builder.openStream();
 
      }
 
    });
 
    return archive;
 
  }

Il file blueprint-example.xml è il file XML che abbiamo definito di sopra: contiene le definizioni dei servizi A e B e dei backing bean BeanA e BeanB.

In questo bundle viene definito un import del package ServiceA.class, questo ci serve per definire una dipendenza tra questo bundle e il primo che abbiamo definito. In questa maniera installare e far partire il bundle BLUEPRINT_BUNDLE permette di farli partire entrambi.

Dopo di ciò si installano le capabilities che ci interessano:

  @Test
 
  @InSequence(0)
 
  public void addBlueprintSupport() throws Exception {
 
    ProvisionerSupport provisioner = new ProvisionerSupport(context);
 
    provisioner.installCapabilities(IdentityNamespace.IDENTITY_NAMESPACE,
                         "aries.blueprint.feature", "jbosgi.jmx.feature");
 
  }

Il test

E ora siamo pronti per eseguire il test vero e proprio:

  @Test
 
  @InSequence(1)
 
  public void testBlueprintContainer() throws Exception {
 
    InputStream input = deployer.getDeployment(BLUEPRINT_BUNDLE);
 
    Bundle bundle = context.installBundle(BLUEPRINT_BUNDLE, input);
 
    try {
 
      bundle.start();
 
      BlueprintContainer container
        = BlueprintSupport.getBlueprintContainer(bundle);
 
      assertNotNull("BlueprintContainer available", container);
 
      ServiceReference srefA
        = context.getServiceReference(ServiceA.class);
 
      assertNotNull("ServiceA not null", srefA);
 
      ServiceA serviceA = context.getService(srefA);
 
      MBeanServer mbeanServer = serviceA.getMbeanServer();
 
      assertNotNull("MBeanServer not null", mbeanServer);
 
      ServiceReference srefB
        = context.getServiceReference(ServiceB.class);
 
      assertNotNull("ServiceB not null", srefB);
 
      ServiceB serviceB = context.getService(srefB);
 
      BeanA beanA = serviceB.getBeanA();
 
      assertNotNull("BeanA not null", beanA);
 
    } finally {
 
      bundle.uninstall();
 
    }
 
  }

Come si vede si installa il bundle del Blueprint, si fa partire il contesto e, infine, si prendono i riferimenti ai servizi e si testano i BeanA e BeanB per verificare che non siano nulli.

Declarative Services

Vediamo ora un semplice esempio di uso del Declarative Service. Supponiamo di voler creare e installare un servizio OSGi di comparazione di oggetti:

public class SampleComparator implements Comparator {
 
  public int compare(Object o1, Object o2) {
 
    return o1.equals(o2) ? 0 : -1;
 
  }
 
}

Dichiariamo ora che il servizio Comparator viene fornito tramite l'implementazione del SampleComparator nel file XML che descrive il componente:

"1.0" encoding="UTF-8"?>
 
"sample.component" immediate="true">
 
  "org.jboss.test.osgi.example.ds.SampleComparator" />
 
  "service.description" value="Sample Comparator Service" />
 
  "service.vendor" value="Apache Software Foundation" />
 
 
 
  "java.util.Comparator" />
 
 

 

Unit Test

Definiamo ora il solito Unit Test con Arquillan, con i soliti deployer e context:

@RunWith(Arquillian.class)
 
public class DeclarativeServicesTestCase {
 

 
  @ArquillianResource
 
  Deployer deployer;
 
  @ArquillianResource
 
  BundleContext context;
 

 
 
}

Dopo di ciò definiamo il nostro bundle che contiene il servizio di comparazione:

   @Deployment
 
  public static JavaArchive dsProvider() {
 
    final JavaArchive archive
      = ShrinkWrap.create(JavaArchive.class, "declarative-services-tests");
 
    archive.addClasses(ProvisionerSupport.class);
 
    archive.addClasses(SampleComparator.class);
 
    archive.addAsResource("repository/felix.scr.feature.xml");
 
    archive.setManifest(new Asset() {
 
      public InputStream openStream() {
 
        OSGiManifestBuilder builder = OSGiManifestBuilder.newInstance();
 
        builder.addBundleSymbolicName(archive.getName());
 
        builder.addBundleManifestVersion(2);
 
        builder.addImportPackages(XRepository.class,
                                  Repository.class, XResource.class,
                                  Resource.class,
                                  XResourceProvisioner.class);
 
        builder.addImportPackages(ServiceTracker.class);
 
        builder.addExportPackages(SampleComparator.class);
 
        return builder.openStream();
 
      }
 
    });
 
    return archive;
 
  }

Adesso dobbiamo definire il servizio DS che contiene il file di configurazione XML che abbiamo visto sopra, e la dipendenza dal servizio ServiceComparator:

@Deployment(name = DS_BUNDLE, managed = false, testable = false)
 
  public static JavaArchive testBundle() {
 
    final JavaArchive archive
      = ShrinkWrap.create(JavaArchive.class, DS_BUNDLE);
 
    archive.addAsResource("ds/OSGI-INF/sample.xml", "OSGI-INF/sample.xml");
 
    archive.setManifest(new Asset() {
 
      public InputStream openStream() {
 
        OSGiManifestBuilder builder = OSGiManifestBuilder.newInstance();
 
        builder.addBundleSymbolicName(archive.getName());
 
        builder.addBundleManifestVersion(2);
 
        builder.addManifestHeader("Service-Component",
                                  "OSGI-INF/sample.xml");
 
        builder.addImportPackages(SampleComparator.class);
 
        return builder.openStream();
 
      }
 
    });
 
    return archive;
 
  }

Con un meccanismo del tutto simile a quello che abbiamo visto nell'esempio precedente, aggiungiamo il servizio di comparazione e installiamo la capability di Declarative service:

  @Test
 
  @InSequence(0)
 
  public void addDeclarativeServicesSupport() throws Exception {
 
    ProvisionerSupport provisioner = new ProvisionerSupport(context);
 
    provisioner.installCapabilities(IdentityNamespace.IDENTITY_NAMESPACE,
                                    "felix.scr.feature");
 
  }

Test di installazione

Adesso non ci rimane che scrivere il test di installazione del servizio del comparatore; in questo test utilizziamo il ServiceTracker per testare che il servizio venga effettivamente deployato nel contesto OSGi:

  @Test
 
  @InSequence(1)
 
  @SuppressWarnings({ "rawtypes" })
 
  public void testImmediateService() throws Exception {
 
    InputStream input = deployer.getDeployment(DS_BUNDLE);
 
    Bundle bundle = context.installBundle(DS_BUNDLE, input);
 
    try {
 
      bundle.start();
 
 
 
      // Track the service provided by the test bundle
 
      final CountDownLatch latch = new CountDownLatch(1);
 
      ServiceTracker tracker =
        new ServiceTracker(context,
                                       Comparator.class.getName(), null) {
 
        public Comparator addingService(
                           ServiceReference reference) {
 
          Comparator service = super.addingService(reference);
 
          latch.countDown();
 
          return service;
 
        }
 
      };
 
      tracker.open();
 
 
 
      // Wait for the service to become available
 
      if (latch.await(2, TimeUnit.SECONDS) == false)
 
        throw new TimeoutException("Timeout tracking Comparator service");
 
    } finally {
 
      bundle.uninstall();
 
    }
 
  }

La callback addingService() verrà chiamata solo quando il servizio di comparazione sarà installata. In caso contrario il Tracker andrà in timeout (2 secondi) e lancerà un errore.

Integrazione di OSGi con lo standard EE

Vediamo adesso degli esempi di integrazione dello standard EE con OSGi; prima di tutto vediamo come ottenere il contesto JNDI di JBoss tramite OSGi, e poi vedremo come integrare un Managed bean (CDI/EJB) con un servizio OSGi.

JNDI

In questo esempio vediamo come ottenere un riferimento JNDI dal contesto OSGi. Prima di tutto, nel nostro Test Case creiamo un archivio che definisca il servizio JNDI:

  @Deployment
 
  public static JavaArchive jndiProvider() {
 
    final JavaArchive archive
      = ShrinkWrap.create(JavaArchive.class, "jndi-tests");
 
    archive.addClasses(ProvisionerSupport.class, NamingSupport.class);
 
    archive.addClasses(JNDITestService.class, JNDITestActivator.class);
 
    archive.addAsResource("repository/aries.blueprint.feature.xml");
 
    archive.addAsResource("repository/aries.jndi.feature.xml");
 
    archive.setManifest(new Asset() {
 
      @Override
 
      public InputStream openStream() {
 
        OSGiManifestBuilder builder = OSGiManifestBuilder.newInstance();
 
        builder.addBundleSymbolicName(archive.getName());
 
        builder.addBundleManifestVersion(2);
 
        builder.addBundleActivator(JNDITestActivator.class);
 
        builder.addImportPackages(XRepository.class, Repository.class,
                                  XResource.class, Resource.class,
                                  XResourceProvisioner.class);
 
        builder.addImportPackages(Context.class,
                                  InitialContextFactory.class);
 
        builder.addDynamicImportPackages(JNDIContextManager.class);
 
        builder.addExportPackages(JNDITestService.class);
 
        return builder.openStream();
 
      }
 
    });
 
    return archive;
 
  }

In questo archivio abbiamo messo un OSGi Activator (JNDITestActivator) che permette di registrare i servizi SimpleInitalContextFactory e StringObjectFactory.

Dopo aver installato questo bundle verifichiamo che possiamo accedere al servizio JNDI:

  @Test
 
  @InSequence(1)
 
  public void testContextManagerOwnerContext(@ArquillianResource Bundle bundle)
    throws Exception {
 
    bundle.start();
 
    // Get the InitialContext via {@link JNDIContextManager}
 
    BundleContext context = bundle.getBundleContext();
 
    ServiceReference sref
      = context.getServiceReference(JNDIContextManager.class);
 
    JNDIContextManager contextManager = context.getService(sref);
 
    Context initialContext = contextManager.newInitialContext();
 
    // Get the context of the owner bundle
 
    BundleContext context
      = (BundleContext) initialContext.lookup(
                        "osgi:framework/bundleContext");
 
    Assert.assertEquals(bundle.getBundleContext(), context);
 
  }

In questo esempio vediamo come sia possibile prendere dal contesto JNDI proprio il contesto OSGi, e viene verificato che sia lo stesso oggetto iniettato da Arquillan al test unitario. Se notiamo bene, nell'esempio precedente riusciamo ad accedere via JNDI ad un servizio OSGi mediante il metodo lookup:

initialContext.lookup("osgi:framework/bundleContext");

Questo è proprio il servizio JNDI Service definito nel Compendium: cioè consiste nel poter accedere ad un servizio OSGi via JNDI. Quindi:

  • OSGi --> JNDI mediante il BundleContext.getServiceReference();
  • JNDI --> OSGi mediante InitialContext.lookup() come specificato nel Compendium.

CDI/EJB integration

Vediamo come integrare un servizio OSGi all'interno di un EJB (Session Bean). Definiamo un servizio, PaymentService come segue:

public interface PaymentService {
 
  String process(String account, Float amount);
 
}

e il suo Activator che crea un servizio OSGi che implementa PaymentService:

public class PaymentActivator implements BundleActivator {
 
  // Provide logging
 
  static final Logger log = Logger.getLogger(PaymentActivator.class);
 
  public void start(BundleContext context) {
 
    log.infof("Start PaymentService");
 
    PaymentService service = new PaymentService() {
 
      public String process(String account, Float amount) {
 
        return "Charged $" + amount + " to account ‘" + account + "‘";
 
      }
 
    };
 
    context.registerService(PaymentService.class.getName(), service, null);
 
  }
 
  public void stop(BundleContext context) {
 
    log.infof("Stop PaymentService");
 
  }
 
}

Vediamo che l'Activator crea un servizio estremamente semplice di PaymentService nel metodo start(), e poi lo registra come servizio nel contesto. Adesso vediamo come integrarlo in un session bean:

@Stateless
 
@LocalBean
 
public class SimpleStatelessSessionBean {
 
  // Provide logging
 
  static final Logger log = Logger.getLogger(SimpleStatelessSessionBean.class);
 
  @Resource
 
  private BundleContext context;
 
  private PaymentService service;
 
  @PostConstruct
 
  public void init() {
 
    final SimpleStatelessSessionBean bean = this;
 
    log.infof("BundleContext symbolic name: %s",
        context.getBundle().getSymbolicName());
 
    // Track {@link PaymentService} implementations
 
    ServiceTracker tracker = new ServiceTracker(context,
      PaymentService.class.getName(), null) {
 
      @Override
 
      public Object addingService(ServiceReference sref) {
 
        log.infof("Adding service: %s to %s", sref, bean);
 
        service = (PaymentService) super.addingService(sref);
 
        return service;
 
      }
 
      @Override
 
      public void removedService(ServiceReference sref, Object sinst) {
 
        super.removedService(sref, service);
 
        log.infof("Removing service: %s from %s", sref, bean);
 
        service = null;
 
      }
 
    };
 
    tracker.open();
 
  }
 
  public String process(String account, String amount) {
 
    if (service == null)
 
      return "PaymentService not available";
 
    return service.process(account,
                       amount != null ? Float.valueOf(amount) : null);
 
  }
 
}

Come vediamo nel session bean, il servizio PaymentService non viene iniettato direttamente dal contenitore EJB, ma viene caricato tramite il BundleContext e il ServiceTracker.

Il metodo init() del session bean carica il servizio in modo asincrono tramite il service tracker, e ottiene così l'istanza al PaymentService creata dall'Activator. In questa maniera nel metodo process() può utilizzarlo correttamente (controllando sempre che il servizio sia stato preventivamente caricato).

Per quanto riguarda i CDI beans, il discorso non cambia: il meccanismo di valorizzazione del servizio OSGi è esattamente lo stesso. Infine, possiamo anche iniettare il servizio EJB che abbiamo visto sopra in una servlet in modo semplice (tramite nome JNDI):

@WebServlet(name="SimpleBeanClientServlet", urlPatterns={"/ejb"})
 
public class SimpleBeanClientServlet  extends HttpServlet {
 
  private static final long serialVersionUID = 1L;
 
  @EJB(lookup = "java:global/example-javaee-ejb3/SimpleStatelessSessionBean")
 
  private SimpleStatelessSessionBean bean;
 
  @Override
 
  protected void doGet(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {
 
    String message = process(req.getParameter("account"),
      req.getParameter("amount"));
 
    PrintWriter out = res.getWriter();
 
    out.println(message);
 
    out.close();
 
  }
 
  private String process(String account, String amount) {
 
    return "Calling SimpleStatelessSessionBean: "
      + bean.process(account, amount);
 
  }
 
}

Quindi è possibile eseguire il metodo process() dell'EJB senza sapere nulla del servizio OSGi sottostante.

Conclusioni

In questo secondo articolo su OSGi abbiamo fatto degli esempi di utilizzo dei servizi definiti nell'OSGi Service Compendium, tra cui Blueprint, Declarative Services, JNDI specification.

Inoltre abbiamo trattato un esempio di integrazione con un ManagedBean (Session Bean, ma avremmo potuto anche definirlo come @Bean CDI, la sostanza non sarebbe cambiata) e abbiamo visto come sia semplice integrare un servizio OSGi dentro un servizio EE. Inoltre è possibile integrare anche altri servizi EE tra cui JPA, JTA service, REST, Web Services... seguendo sempre la falsariga che abbiamo visto con l'esempio del servizio JNDI.

Per esempio per ottenere un EntityManagerFactory basta fare:

ServiceReference sref =
context.getServiceReference(EntityManagerFactory.class);

In definitiva, abbiamo visto come non sia del tutto complicato integrare i due mondi OSGi ed EE, e inoltre abbiamo visto come il servizio Blueprint semplifichi enormemente la definizione di servizi OSGi utilizzando praticamente la stessa sintassi che usa Spring nei suoi file di configurazione XML.

 

Condividi

Pubblicato nel numero
186 luglio 2013
Michele Mazzei si è laureato in Scienze dell’Informazione nell’ormai lontano 1998. Si occupa di progettazione e scrittura di software in Java/Java EE e in C/C++ sul mondo Linux. Lavora a Roma in ambito spaziale maturando esperienze in ambito OGC, GIS, Map Server, Payload Data Ground Segment (PDGS). Si interessa di…
Articoli nella stessa serie
Ti potrebbe interessare anche
Seguici

redazionemokabyte.it
MokaByte è un marchio registrato da Imola Informatica S.P.A.
Via Selice 66/a 40026 Imola (BO) C.F. e Iscriz.Registro imprese BO 03351570373 P.I. 00614381200 - Cap. Soc. euro 100.000,00 i.v