Saturday, September 7, 2013

Distributed OSGi services in JBoss Fuse

JBoss Fuse (Fuse ESB) is a great container featuring Apache Karaf as OSGi container. It has a great feature called Fabric, on which you can start another container efficiently and do provisioning from any container connected in one Fabric. It means that, you can uninstall bundles from in other container from your current container.

Fabric also allows a bundle to publish services and let them be consumed by other bundles. Not just by bundles in the same container, but in other bundles as long as publishers and consumers are connected in one Fabric.

I'm here not to to tell you how to use JBoss Fuse or what it is. But to tell you that it's been hard to find references about how to publish services and consume them in distributed environment. So, I want to share my experience to handle that with blueprint or with Spring DM 1.x.x based on scattered sources of information I have read for weeks.



Let's get started!

Assume that you have a publisher which publishes a service to reverse text. It is published in container child1. Not only that, but you also have a consumer to the service in other container named child2. Child1 and child2 are children from a Fabric. You can create them in the root container by using

fabric:create --clean
fabric:container-create-child root child 2

Back to the service, you have to create 3 bundles:
  • An API jar, which defines interfaces used by the publisher and the consumer.
  • A publisher OSGi bundle, which creates the implementations and publish them as services.
  • A consumer OSGi bundle, which consumes the services.
Note that is is recommended to split APIs and services into two separated bundles.


The API jar

It only contains one java file: src/main/java/**/StringReverseService.java

package id.web.michsan.fuse.api;

public interface StringReverseService {

    public String reverse(String text);

}


The Publisher

It contains the implementation of the interface which is also one java file src/main/java/**/StringReverseServiceImpl.java

package id.web.michsan.fuse.publisher;

import id.web.michsan.fuse.api.StringReverseService;

public class StringReverseServiceImpl implements StringReverseService {

    public String reverse(String text) {
        return new StringBuffer(text).reverse().toString();
    }

}

It also contains OSGi service definition in blueprint format. You should prefer Blueprint to Spring DM as it's standard and widely supported by OSGi containers.

src/main/resources/OSGI-INF/blueprint/bundle-context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">

 <bean id="myStringReverseService"
  class="id.web.michsan.fuse.publisher.StringReverseServiceImpl">
  <!-- <property name="verbose" value="${verbose}" /> -->
 </bean>

</blueprint>

src/main/resources/OSGI-INF/blueprint/bundle-context-osgi.xml:
<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
 xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

 <!-- Load configuration file defined in persistent-id -->
 <cm:property-placeholder persistent-id="id.web.michsan.fuse.publisher">
  <cm:default-properties>
   <cm:property name="verbose" value="false" />
  </cm:default-properties>
 </cm:property-placeholder>

 <!-- Export as OSGi services -->
 <service ref="myStringReverseService"
  interface="id.web.michsan.fuse.api.StringReverseService">
  <service-properties>
   <entry key="service.exported.interfaces" value="*" />
  </service-properties>
 </service>

</blueprint>

The key service.exported.interfaces in <service-properties>  tag is so important as it lets your service be available from other container.

In the pom.xml, aside of using <packaging>bundle</packaging>, you need to include:
<build>
   <plugins>
      <!-- OSGi bundle generation -->
      <plugin>
         <groupId>org.apache.felix</groupId>
         <artifactId>maven-bundle-plugin</artifactId>
         <version>2.3.7</version>
         <extensions>true</extensions>
         <configuration>
            <instructions>
               <Private-Package>id.web.michsan.fuse.publisher.*</Private-Package>
               <Import-Package>
                  org.springframework.aop,
                  org.springframework.aop.framework,
                  org.aopalliance.aop,
                  *
               </Import-Package>
               <Export-Package></Export-Package>
               <_removeheaders>Import-Service,Export-Service</_removeheaders>
               <Bundle-Description>${project.description}</Bundle-Description>
            </instructions>
         </configuration>
      </plugin>
   </plugins>
</build>

The Consumer
In consumer bundle, you have to create a class which consumes the service:
package id.web.michsan.fuse.consumer;

import id.web.michsan.fuse.api.StringReverseService;

import java.net.InetSocketAddress;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.SocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

public class RequestHandler {

   private int port;
   private StringReverseService stringReverseService;

   private SocketAcceptor acceptor;

   @PostConstruct
   public void afterPropertiesSet() throws Exception {
      acceptor = new NioSocketAcceptor();

      acceptor.getFilterChain().addLast("textLineCodec",
            new ProtocolCodecFilter(new TextLineCodecFactory()));

      acceptor.setHandler(new IoHandlerAdapter() {
         @Override
         public void messageReceived(IoSession session, Object message)
               throws Exception {
            String text = (String) message;
            String result = stringReverseService.reverse(text);
            session.write(result);
         }
      });

      acceptor.setReuseAddress(true);
      acceptor.bind(new InetSocketAddress(port));
   }

   @PreDestroy
   public void destroy() throws Exception {
      if (acceptor != null)
         acceptor.unbind();
   }

   /** ---- Accessors ---------------------------------- */

   public void setPort(int port) {
      this.port = port;
   }

   public void setStringReverseService(
         StringReverseService stringReverseService) {
      this.stringReverseService = stringReverseService;
   }

}

For the src/main/resources/OSGI-INF/blueprint/bundle-context-osgi.xml you should create like the following:
<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
   xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.0.0">

   <!-- Load configuration from file defined by persistent-id attribute -->
   <cm:property-placeholder persistent-id="id.web.michsan.fuse.consumer">
      <cm:default-properties>
         <cm:property name="port" value="4321" />
      </cm:default-properties>
   </cm:property-placeholder>

   <!-- Import OSGi services -->
   <reference id="bokuNoStringReverseService"
      interface="id.web.michsan.fuse.api.StringReverseService" />

</blueprint>

While the src/main/resources/OSGI-INF/blueprint/bundle-context.xml is:
<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">

   <bean class="id.web.michsan.fuse.consumer.RequestHandler" init-method="afterPropertiesSet">
      <property name="port" value="${port}" />
      <property name="stringReverseService" ref="bokuNoStringReverseService" />
      <property name="userDao" ref="bokuNoUserDao" />
   </bean>

</blueprint>
The features definition file
When using Fabric, we have to install all features with profile

fuse-features.xml:

<features>
   <feature name="michsan-publisher" version="1.0.0">
      <bundle>fab:mvn:id.web.michsan.fuse/fuse-api/0.0.1-SNAPSHOT</bundle>
      <bundle>mvn:id.web.michsan/fuse-publisher/0.0.1-SNAPSHOT</bundle>
   </feature>

   <feature name="michsan-consumer" version="1.0.0">
      <bundle>fab:mvn:id.web.michsan.fuse/fuse-api/0.0.1-SNAPSHOT</bundle>
      <bundle>mvn:org.apache.mina/mina-core/2.0.7</bundle>
      <bundle>mvn:id.web.michsan.fuse/fuse-consumer/0.0.1-SNAPSHOT</bundle>
   </feature>
</features>

Installation

From a fresh JBoss Fuse Full zip file, start a default container (./bin/fuse)

fabric:create --clean

After setting your password, wait for a while until all required bundles started (check from command: list)

Create children containers.
fabric:container-create-child root child 2

As we know, we should use profile to install features
fabric:profile-create --parents jboss-fuse-minimal --parents dosgi michsan-base
fabric:profile-edit -r file:///path/to/michsan-features.xml michsan-base

fabric:profile-create --parents michsan-base michsan-publisher
fabric:profile-edit -f michsan-publisher michsan-publisher

fabric:profile-create --parents michsan-base michsan-consumer
fabric:profile-edit -f michsan-consumer michsan-consumer

Install into two different containers after both of them started well (check from command: container-list)
fabric:container-add-profile child1 michsan-publisher
fabric:container-add-profile child2 michsan-consumer

1 comment:

albina N muro said...

Fabric also allows a bundle to publish services and let them be consumed by other bundles. Not just by bundles in the same container, but in other bundles as long as publishers and consumers are connected in one Fabric. kontraktor murah di jogja