Monday Jan 10, 2011

TOTD #154: Dynamic OSGi services in GlassFish 3.1 - Using CDI and @OSGiService

TOTD #131 explained how dynamic OSGi services can be easily created and deployed in GlassFish. The dynamism comes from the fact that the client keeps a reference to the service which is automatically refreshed if the bundle providing the service is refreshed. That tip used ServiceTracker API for the dynamic discovery of service which still uses some boilerplate code. In Java EE 6 environment, this can be further simplified by using CDI to inject the service and delegate all the service discovery/bind code to a CDI Qualifier available using CDI extensions in GlassFish 3.1.

Siva provides more details in his blog. But basically GlassFish 3.1 comes with a a standard CDI portable extension (org.glassfish.osgi-cdi) that intercepts deployment of hybrid applications that has components who have expressed dependencies on OSGi services. This CDI extension then takes care of discover, bind, inject, and track the service.

With this new CDI extension, the boilerplate code of dynamically tracking the service changes from:

ServiceTracker tracker = new ServiceTracker(context, Hello.class.getName(), null);
tracker.open();
Hello hello = (Hello) tracker.getService();
System.out.println(hello.sayHello("Duke"));

to

@Inject @OSGiService(dynamic=true) Hello hello;
System.out.println(hello.sayHello("Duke"));

No String-based and completely typesafe resolution of an OSGi service in a web application, how neat!

This will work in a "hybrid application" only, and not in pure web application, as BundleContext is required for dynamically tracking the service. Notice that by default the application is not ready to handle the dynamicity of OSGi environment and so "dynamic=true" need to be set explicitly on @OSGiService.

The complete source code for this application is available here. This project consists of 4 Maven modules:

  • helloworld-api
  • helloworld-impl
  • helloworld-client
  • helloworld-cdiclient

The first three modules are explained in detail at TOTD #131. Lets create "helloworld-cdiclient" module!

  1. In the parent directory of TOTD #131 zip file, create a new Maven module as:
    mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=org.samples.osgi.helloworld -DartifactId=helloworld-webclient
    

    Make sure to change the all the references from "totd131" to "totd154".
  2. The updated directory structure looks like:
    osgi.properties
    pom.xml
    src
    src/main
    src/main/java
    src/main/java/org
    src/main/java/org/samples
    src/main/java/org/samples/osgi
    src/main/java/org/samples/osgi/helloworld
    src/main/java/org/samples/osgi/helloworld/App.java
    src/main/resources
    src/main/webapp
    src/main/webapp/index.jsp
    src/main/webapp/WEB-INF
    src/main/webapp/WEB-INF/beans.xml
    
    1. "index.jsp" is generated by the Maven archetype
    2. "beans.xml" is a newly added file to enable CDI injection
    3. "osgi.properties" is a newly added file and is used to read the metadata for OSGi hybrid application
    4. "App.java" is a Servlet that invokes the service
  3. The "osgi.properties" file looks like:
    Web-ContextPath:/helloworld-cdiclient
    
    This makes sure that the hybrid application is accessible at "/helloworld-cdiclient" context root.
  4. The updated "pom.xml" looks like:
    <?xml version="1.0" encoding="UTF-8"?>
    <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
             http://maven.apache.org/xsd/maven-4.0.0.xsd" 
             xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <modelVersion>4.0.0</modelVersion>
        <parent>
          <artifactId>totd154</artifactId>
          <groupId>org.samples.osgi.helloworld</groupId>
          <version>1.0-SNAPSHOT</version>
        </parent>
        <packaging>war</packaging>
        <groupId>org.samples.osgi.helloworld</groupId>
        <artifactId>helloworld-cdiclient</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>helloworld-cdiclient</name>
        <url>http://maven.apache.org</url>
        <dependencies>
          <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
          </dependency>
          <dependency>
            <groupId>org.samples.osgi.helloworld</groupId>
            <artifactId>helloworld-api</artifactId>
          </dependency>
          <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
          </dependency>
          <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>osgi-cdi-api</artifactId>
            <version>3.1-SNAPSHOT</version>
            <scope>provided</scope>
          </dependency>
        </dependencies>
        <build>
          <pluginManagement>
            <plugins>
              <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.1.0</version>
                <extensions>true</extensions>
                <configuration>
                  <supportedProjectTypes>
                    <supportedProjectType>ejb</supportedProjectType>
                    <supportedProjectType>war</supportedProjectType>
                    <supportedProjectType>bundle</supportedProjectType>
                    <supportedProjectType>jar</supportedProjectType>
                  </supportedProjectTypes>
                  <instructions>
                    <!-- Read all OSGi configuration info from this optional file -->
                    <_include>-osgi.properties</_include>
                    <!-- By default, we don't export anything -->
                    <Export-Package>!\*.impl.\*, \*</Export-Package>
                  </instructions>
                </configuration>
                <executions>
                  <execution>
                    <id>bundle-manifest</id>
                    <phase>process-classes</phase>
                    <goals>
                      <goal>manifest</goal>
                    </goals>
                  </execution>
                  <execution>
                    <id>bundle-install</id>
                    <phase>install</phase>
                    <goals>
                      <goal>install</goal>
                    </goals>
                  </execution>
                </executions>
              </plugin>
              <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                  <source>1.5</source>
                  <target>1.5</target>
                </configuration>
              </plugin>
              <plugin> <!-- Need to use this plugin to build war files -->
                <artifactId>maven-war-plugin</artifactId>
                <groupId>org.apache.maven.plugins</groupId>
                <!-- Use version 2.1-beta-1, as it supports the new property failOnMissingWebXml -->
                <version>2.1-beta-1</version>
                <configuration>
                  <archive>
                    <!-- add bundle plugin generated manifest to the war -->
                    <manifestFile>
                      ${project.build.outputDirectory}/META-INF/MANIFEST.MF
                    </manifestFile>
                    <!-- For some reason, adding Bundle-ClassPath in maven-bundle-plugin
                         confuses that plugin and it generates wrong Import-Package, etc.
                         So, we generate it here.
                    -->
                    <manifestEntries>
                      <Bundle-ClassPath>WEB-INF/classes/
                      </Bundle-ClassPath>
                    </manifestEntries>
                  </archive>
                  <!-- We don't have a web.xml -->
                  <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
              </plugin>
            </plugins>
          </pluginManagement>
          <plugins>
            <plugin>
              <groupId>org.apache.felix</groupId>
              <artifactId>maven-bundle-plugin</artifactId>
            </plugin>
          </plugins>
       </build>
    </project>
    

    Screencast #32 provide a detailed explanation of this "pom.xml". A basic introduction to "hybrid applications" is given here.
  5. The updated "App.java" looks like:
    package org.samples.osgi.helloworld;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    import javax.inject.Inject;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.glassfish.osgicdi.OSGiService;
    import org.samples.osgi.helloworld.api.Hello;
    
    /\*\*
     \* Hello world!
     \*/
    @WebServlet(urlPatterns = {"/HelloWebClient"})
    public class App extends HttpServlet {
        
        @Inject @OSGiService(dynamic=true) Hello hello;
    
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws IOException, ServletException {
            PrintWriter out = response.getWriter();
            out.println(hello.sayHello("Duke"));
        }
    }
    

    This is a "web.xml"-free servlet using @WebServlet. "@OSGiService" is used to dynamically discover, bind, inject, and track the service. So if the backend service changes then the service reference is automatically updated without any additional effort.
  6. Build and Deploy this project as:
    mvn clean install
    asadmin deploy --type=osgi  target/helloworld-cdiclient-1.0-SNAPSHOT.war
    
    Optionally you can also drop this generated WAR file to "glassfish/domains/domain1/autodeploy/bundles" directory as well.
  7. Run the app
    1. Make sure helloworld-api-\* and helloworld-impl-\* bundles are already deployed as explained in TOTD #131.
    2. Invoking "curl http://localhost:8080/helloworld-cdiclient/HelloWebClient" shows the result as "Hello Duke".
    3. Change "org.samples.osgi.helloworld.impl.HelloImpl" to use "Howdy" string instead of "Hello", build the bundle again (mvn clean install), and copy it to the "glassfish/domains/domain1/autodeploy/bundles" directory.
    4. Invoking "curl http://localhost:8080/helloworld-cdiclient/HelloWebClient" now shows the result as "Howdy Duke". This ensures that the service changes are dynamically tracked.

I tried this with GlassFish 3.1 build 35 Web Profile.

Read the latest about GlassFish and OSGi integration on this wiki.

Where are you deploying your OSGi applications ?

Technorati: totd osgi javaee6 glassfish dynamic discovery

Wednesday Apr 28, 2010

TOTD #131: Dynamic OSGi services in GlassFish - Using ServiceTracker

OSGi is the dynamic module system for Java. Each module, or bundle as called in OSGi terminology, is packaged as a JAR file and the inbound and outbound dependencies are explicitly defined using the META-INF/MANIFEST.MF in the JAR file. A complex software system may be broken into multiple modules where each module may be exposing a set of services. These services are then consumed by some other "client" OSGi bundles. The beauty of dynamic nature of OSGi is that each bundle can be easily updated without restarting the framework and any reference to the service in the "client" bundles is also updated accordingly. However the client needs to ensure that they are watching out for changes in the lifecycle of "service" bundle.

GlassFish's modular architecture is based upon OSGi. The different capabilities such as Web container, EJB container, and RESTful Web services are provided as OSGi modules. The OSGi framework is available to developers as well so that they can construct their application as OSGi modules and leverage all the goodness.

This Tip Of The Day (TOTD) explains an OSGi design pattern that allows the client to track the dynamic discovery of an OSGi service. This means that client keeps a reference to the service which is automatically refreshed if the bundle providing the service is refreshed. And of course, it uses GlassFish to deploy the bundles :-)

For those, who prefer to see the results directly, download and unzip this zip file:

  1. Give the following command in the root directory:
    mvn clean install
    
  2. Copy the generated bundles to "domains/domain1/autodeploy/bundles" directory as:
    cp helloworld-api/target/helloworld-api-1.0-SNAPSHOT.jar \\
    helloworld-impl/target/helloworld-impl-1.0-SNAPSHOT.jar \\
    helloworld-client/target/helloworld-client-1.0-SNAPSHOT.jar \\
    ~/tools/glassfish/v3/final/glassfishv3/glassfish/domains/domain1/autodeploy/bundles
    

    The following log messages will be displayed in the console:
    [#|2010-04-28T17:03:55.090-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
    services.impl|_ThreadID=23;_ThreadName=Thread-23;|Getting a new service|#]
    
    [#|2010-04-28T17:03:55.091-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
    services.impl|_ThreadID=23;_ThreadName=Thread-23;|Hello Duke|#]
    
    [#|2010-04-28T17:03:57.091-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
    services.impl|_ThreadID=23;_ThreadName=Thread-23;|Getting a new service|#]
    
    [#|2010-04-28T17:03:57.092-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
    services.impl|_ThreadID=23;_ThreadName=Thread-23;|Hello Duke|#]
    
    

    The log messages from the client bundle invoking the service bundle are highlighted in bold.
  3. Update the service implementation
    1. Edit service implementation in "hello-impl/src/main/java/org/samples/osgi/helloworld/impl/HelloImpl.java" and change the "return" statement from:
      return "Hello " + name;
      
      to
      return "Howdy " + name;
      
    2. Create the service implementation bundle again by giving the command:
      mvn clean install
      

      in the "helloworld-impl" directory.
    3. Copy the updated bundle from "helloworld-impl/target/helloworld-impl-1.0-SNAPSHOT.jar" to "glassfishv3/glassfish/domains/domain1/autodeploy/bundles" directory. The following sequence of log messages will be seen:
      [#|2010-04-28T17:04:47.110-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
      services.impl|_ThreadID=23;_ThreadName=Thread-23;|Getting a new service|#]
      
      [#|2010-04-28T17:04:47.110-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
      services.impl|_ThreadID=23;_ThreadName=Thread-23;|Hello Duke|#]
      
      [#|2010-04-28T17:04:48.151-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
      services.impl|_ThreadID=22;_ThreadName={felix.fileinstall.poll=5000, 
      felix.fileinstall.bundles.new.start=true, service.pid=org.apache.felix.fileinstall.b0b03c1b-5a58-457d-bfde-116be31299f0,
      felix.fileinstall.dir=/Users/arungupta/tools/glassfish/v3/final/glassfishv3/glassfish/domains/domain1
      /autodeploy/bundles/, felix.fileinstall.filename=org.apache.felix.fileinstall-autodeploy-bundles.cfg, 
      service.factorypid=org.apache.felix.fileinstall, felix.fileinstall.debug=1};|Updated 
      /Users/arungupta/tools/glassfish/v3/final/glassfishv3/glassfish/domains/domain1/autodeploy/bundles/
      helloworld-impl-1.0-SNAPSHOT.jar|#]
      
      [#|2010-04-28T17:04:49.110-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
      services.impl|_ThreadID=23;_ThreadName=Thread-23;|Getting a new service|#]
      
      [#|2010-04-28T17:04:49.111-0700|INFO|glassfishv3.0|javax.enterprise.system.std.com.sun.enterprise.v3.
      services.impl|_ThreadID=23;_ThreadName=Thread-23;|Howdy Duke|#]
      

      As evident from the log messages, "Hello Duke" message is printed first, the service implementation bundle gets refreshed, and then the message from the updated service implementation, i.e. "Howdy Duke" is printed. Notice, only the service implementation got refreshed.

Now some explanation!

The application is split into 3 bundles - API, Impl, and Client. Splitting into 3 bundles allows cleaner separation and other implementations of the serviceĀ  to show up relying purely upon the API bundle.

The "API" bundle (helloworld-api) has one class with the following interface:

package org.samples.osgi.helloworld.api;

public interface Hello {
    public String sayHello(String name);
}

This bundle has the following manifest entry:

<Export-Package>${pom.groupId}.api</Export-Package>

to ensure that the package with the service interface is exported and available for imports by service implementers. The "Impl" bundle (helloworld-impl) has the trivial implementation of this interface as:
package org.samples.osgi.helloworld.impl

import org.samples.osgi.helloworld.api.Hello;

public class HelloImpl implements Hello {
    public String sayHello(String name) {
        return "Hello " + name;
    }
}

And the bundle's activator registers the service as:

package org.samples.osgi.helloworld.impl;

import java.util.Properties;
import org.samples.osgi.helloworld.api.Hello;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class App implements BundleActivator {

    public void start(BundleContext bc) throws Exception {
        bc.registerService(Hello.class.getName(), new HelloImpl(), new Properties());
    }

    public void stop(BundleContext bc) throws Exception {
        bc.ungetService(bc.getServiceReference(Hello.class.getName()));
    }
}

This bundle defines the dependency on the package exported earlier as:

<dependency>
 <groupId>${pom.groupId}</groupId>
 <artifactId>helloworld-api</artifactId>
 <version>1.0-SNAPSHOT</version>
 </dependency>

and also imports the appropriate packages as:

<Import-Package>${pom.groupId}.api, org.osgi.framework</Import-Package>

The "client" bundle's (helloworld-client) activator uses "org.osgi.util.tracker.ServiceTracker" for the dynamic discovery of service. The code looks like:

package org.samples.osgi.helloworld.client;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.samples.osgi.helloworld.api.Hello;

public class App implements BundleActivator {
    BundleContext ctx;
    ServiceTracker tracker;

    public void start(BundleContext context) {
        System.out.println("Starting client bundle");

        this.ctx = context;        

        // Create a service tracker to monitor Hello services.
        tracker = new ServiceTracker(context, Hello.class.getName(), null);
        tracker.open();

        new PingService(tracker).start();
    }

    public void stop(BundleContext context) {
        System.out.println("Stopping client bundle");
        tracker.close();
    }

    private class PingService extends Thread {
        ServiceTracker tracker;

        PingService(ServiceTracker tracker) {
            this.tracker = tracker;
        }

        public void run() {
            try {
                while (true) {
                    Thread.sleep(2000);
                    System.out.println("Getting a new service");
                    Hello hello = (Hello) tracker.getService();
                    if (hello == null)
                        System.out.println("No service found!"); 
                    else      
                        System.out.println(hello.sayHello("Duke"));
                }
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }
}

The ServiceTracker API allows customized service tracking and find services that meet search criteria defined by filters. Basically it listens to different service events and gets/ungets the service accordingly. In this case, a "ServiceTracker" is created to track the service by specifying the class name. The "tracker.open()" starts the service tracking. A new thread is started which pings the service every 2 seconds and prints the received response. Notice, a new service is retrieved from the service tracker as the service bundle might have been refreshed.

Please read through OSGi javadocs for more details on these APIs.

A future blog will show how a Java EE 6 MVC-based application can be split into multiple OSGi bundles using this design pattern.

How are you using dynamic discovery of services in OSGi ?

Technorati: totd osgi modules glassfish v3 felix dynamic discovery

About

profile image
Arun Gupta is a technology enthusiast, a passionate runner, author, and a community guy who works for Oracle Corp.


Java EE 7 Samples

Stay Connected

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today