Getting Started with OSGi Remote Services - Bndtools Edition

38 minute read

At the EclipseCon Europe 2016 I held a tutorial together with Peter Kirschner named Building Nano Services with OSGi Declarative Services. The final exercise should have been the demonstration of OSGi Remote Services. It actually did not really happen because of the lack of time and networking issues. The next year at the EclipseCon Europe 2017 we joined forces again and gave a talk with the name Microservices with OSGi. In that talk we focused on OSGi Remote Services, but we again failed with the demo at the end because of networking issues. At the EclipseCon Europe 2018 I gave a talk on how to use different OSGi specifications for connecting services remotely titled How to connect your OSGi application. Of course I mentioned OSGi Remote Services there, and of course the demonstration failed again because of networking issues.

In the last years I published several blog posts and gave several talks related to OSGi, and often the topic OSGi Remote Services was raised, but never really covered in detail. Scott Lewis, the project lead of the Eclipse Communication Framework, was really helpful whenever I encountered issues with Remote Services. I promised to write a blog post about that topic as a favour for all the support. And with this blog post I finally want to keep my promise. That said, let’s start with OSGi Remote Services.

Motivation

First I want to explain the motivation for having a closer look at OSGi Remote Services. Looking at general software architecture discussions in the past, service oriented architectures and microservices are a huge topic. Per definition the idea of a microservices architecture is to have

  • a suite of small services
  • each running in its own process
  • communicating with a lightweight mechanism, e.g. HTTP
  • independently deployable
  • easy to replace

While new frameworks and tools came up over the years, the OSGi specification(s) covers these topics for a long time. Via the service registry and the service dynamics you can build up very small modules. Those modules can then be integrated into small runtimes and deployed in different environments (despite the needed JVM or a database if needed). The services in those small independent deployments can then be accessed in different ways, like using the HTTP Whiteboard or JAX-RS Whiteboard. This satisfies the aspect of a communication between services via lightweight mechanisms. For inhomogeneous environments the usage of those specifications is a good match. But it means that you need to implement the access layer on the provider side (e.g. the JAX-RS wrapper to access the service via REST) and you need to implement the service access on the consumer side by using a corresponding framework to execute the REST calls.

Ideally the developer of the service as well as the developer of the service consumer should not need to think about the infrastructure of the whole application. Well, it is always good that everybody in a project knows about everything, but the idea is to not making your code dependent on infrastructure. And this is where OSGi Remote Services come in. You develop the service and the service consumer as if they would be executed in the same runtime. In the deployment the lightweight communication will be added to support service communication over a network.

And as initially mentioned, I want to have a look at ways how to probably get rid of the networking issues I faced in the presentations in the past.

Introduction

To understand this blog post you should be familiar with OSGi services and ideally with OSGi Declarative Services. If you are not familiar with OSGi DS, you can get an introduction by reading my blog post Getting Started with OSGi Declarative Services.

In short, the OSGi Service Layer specifies a Service Producer that publishes a service, and a Service Consumer that listens and retrieves a service. This is shown in the following picture:

With OSGi Remote Services this picture is basically the same. The difference is that the services are registered and consumed across network boundaries. For OSGi Remote Services the above picture could be extended to look like the following:

Glossary

To understand the above picture and the following blog post better, here is a short glossary for the used terms:

  • Remote Service (Distributed Service)
    Basic specification to describe how OSGi services can be exported and imported to be available across network boundaries.
  • Distribution Provider
    Exports services by creating endpoints on the producer side, imports services by creating proxies to access endpoints on the consumer side, manages policies around the topology and discovers remote services.
  • Endpoint
    Communication access mechanism to a remote service that requires some protocol for communications.
  • Topology
    Mapping between services and endpoints as well as their communication characteristics.
  • Remote Service Admin (RSA)
    Provides the mechanisms to import and export services through a set of configuration types. It is a passive Distribution Provider, not taking any action to export or import itself.
  • Topology Manager
    Provides the policy for importing and exporting services via RSA and implements a Topology.
  • Discovery
    Discover / announce Endpoint Descriptions via some discovery protocol.
  • Endpoint Description
    A properties based description of an Endpoint that can be exchanged between different frameworks to create connections to each other’s services.

To get a slightly better understanding, the following picture shows some more details inside the Remote Service Implementation block.

Note:
Actually this picture is still a simplified version, as internally there are Endpoint Event Listener and Remote Service Admin Listener that are needed to trigger all the necessary actions. But to get an idea how things play together this picture should be sufficient.

Now let’s explain the picture in more detail:

Service Provider Runtime

  • A service is marked to be exported. This is done via service properties.
  • The Distribution Provider creates an endpoint for the exported service:
    • The Topology Manager gets informed about the exported service.
    • If the export configuration matches the Topology it instructs the Remote Service Admin to create an Endpoint.
    • The Remote Service Admin creates the Endpoint.
  • The Discovery gets informed via Endpoint Event Listener and announces the Endpoint to other systems via Endpoint Description.

Service Consumer Runtime

  • The Discovery discovers an Endpoint via Endpoint Description that was announced in the network.
  • The Distribution Provider creates a proxy for the service.
    • The Topology Manager learns from the Discovery about the newly discovered service (via Endpoint Event Listener), which then instructs the Remote Service Admin to import the service.
    • The Remote Service Admin then creates a local service proxy that is registered as service in the local OSGi runtime. This proxy is mapped to the remote service (or an alternative like a webservice).
  • The service proxy is used for wiring.

To simplify the picture again, the important takeaways are the Distribution Provider and the Discovery. The Distribution Provider is responsible for exporting and importing the service, the Discovery is responsible for announcing and discovering the service. The other terms are needed for a deeper understanding, but for a high level understanding of OSGi Remote Services, these two are sufficient.

Tutorial

Now it is time to get our hands dirty and play with OSGi Remote Services. This tutorial has several steps:

  1. Project Setup
  2. Service Implementation (API & Impl)
  3. Service Provider Runtime
  4. Service Consumer Implementation
  5. Service Consumer Runtime

There are different ways and tools available for OSGi development. In this tutorial I will use Bndtools. I also published this tutorial with other toolings if you don’t want to use Bndtools:

ECF - Remote Service Runtime

While the implementation and export of an OSGi service as a Remote Service is trivial in first place, the definition of the runtime can become quite complicated. Especially collecting the necessary bundles is not that easy without some guidance.

As a reference, with Equinox as underlying OSGi framework the following bundles need to be part of the runtime as a basis:

  • Equinox OSGi
    • org.eclipse.osgi
    • org.eclipse.osgi.services
    • org.eclipse.equinox.common
    • org.eclipse.equinox.event
    • org.eclipse.osgi.util
    • org.apache.felix.scr
  • Equinox Console
    • org.apache.felix.gogo.command
    • org.apache.felix.gogo.runtime
    • org.apache.felix.gogo.shell
    • org.eclipse.equinox.console
  • ECF and dependencies
    • org.eclipse.core.jobs
    • org.eclipse.ecf
    • org.eclipse.ecf.discovery
    • org.eclipse.ecf.identity
    • org.eclipse.ecf.osgi.services.distribution
    • org.eclipse.ecf.osgi.services.remoteserviceadmin
    • org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy
    • org.eclipse.ecf.remoteservice
    • org.eclipse.ecf.remoteservice.asyncproxy
    • org.eclipse.ecf.sharedobject
    • org.eclipse.equinox.concurrent
    • org.eclipse.osgi.services.remoteserviceadmin

With the above basic runtime configuration the Remote Services will not yet work. There are still two things missing, the Discovery and the Distribution Provider. ECF provides different implementations for both. Which implementations to use needs to be defined by the project. In this tutorial we will use Zeroconf/JmDNS for the Discovery and the Generic Distribution Provider:

  • ECF Discovery - Zeroconf
    • org.eclipse.ecf.provider.jmdns
  • ECF Distribution Provider - Generic
    • org.eclipse.ecf.provider
    • org.eclipse.ecf.provider.remoteservice

Note:
You can find the list of different implementations with the documentation about the bundles, configuration types and intents in the ECF Wiki:

Project Setup

ECF Templates

The project setup with Bndtools is different compared to PDE tooling. With Bndtools you setup a Workspace and configure the repositories to use. The ECF project provides Workspace/Project Templates to make the setup easier.

  • Add the ECF Bndtools Workspace Template
    • Window -> Preferences -> Bndtools-> Workspace Template
    • In the GitHub Repositories section select the green plus on the right to add a new repository
    • Set Repository Name: to ECF/bndtools.workspace
    • Leave the Branch: empty to use the default branch
    • Select Validate to check if the ECF Workspace Template can be loaded
    • Select Save
    • Select Apply and close to close the Preferences Window
  • Create a Remote Services Bndtools Workspace
    • File -> New -> Other… -> Bndtools -> Bnd OSGi Workspace
    • Select the Location where the Bnd Workspace should be created
    • Select the Template GitHub -> ECF/bndtools.workspace
    • Select Next to load the ECF Workspace Template
    • Select Finish to finalize the workspace creation

Further details on the Bndtools support provided by the ECF project can be found in the Eclipse Wiki.

Bnd OSGi Templates

Alternatively to using the provided ECF Bndtools Templates, you can configure the workspace manually. This might be useful as the ECF Templates add everything ECF provides to the workspace (including examples). That is perfect for getting started and learning the topics, but for more experienced setups this is probably too much as you want to limit your repository to what you really need.

For the manual setup you create a BND OSGi Workspace by using the default bndtools/workspace:

  • Create a Bnd OSGi Workspace
    • File -> New -> Other… -> Bndtools -> Bnd OSGi Workspace
    • Select the Location where the Bnd Workspace should be created
    • Click Next
    • Select the Template GitHub -> bndtools/workspace
    • Select Next to load the workspace template
    • Select Finish to finalize the workspace creation

To add the ECF related artifacts you need to modify some files in the workspace:

  • Create the file ecfatcentral.maven in the cnf folder
  • Add the following content to the file
# ECF
org.eclipse.platform:org.eclipse.core.jobs:3.12.0
org.eclipse.platform:org.eclipse.equinox.common:3.15.100
org.eclipse.platform:org.eclipse.equinox.concurrent:1.2.100
org.eclipse.ecf:org.eclipse.ecf:3.10.0
org.eclipse.ecf:org.eclipse.ecf.console:1.3.100
org.eclipse.ecf:org.eclipse.ecf.discovery:5.1.1
org.eclipse.ecf:org.eclipse.ecf.identity:3.9.402
org.eclipse.ecf:org.eclipse.ecf.osgi.services.distribution:2.1.600
org.eclipse.ecf:org.eclipse.ecf.osgi.services.remoteserviceadmin:4.9.3
org.eclipse.ecf:org.eclipse.ecf.osgi.services.remoteserviceadmin.console:1.3.0
org.eclipse.ecf:org.eclipse.ecf.osgi.services.remoteserviceadmin.proxy:1.0.101
org.eclipse.ecf:org.eclipse.ecf.remoteservice.asyncproxy:2.1.200
org.eclipse.ecf:org.eclipse.ecf.remoteservice:8.14.0
org.eclipse.ecf:org.eclipse.ecf.sharedobject:2.6.200
org.eclipse.ecf:org.eclipse.osgi.services.remoteserviceadmin:1.6.300

# ECF Discovery Zeroconf
org.eclipse.ecf:org.eclipse.ecf.provider.jmdns:4.3.301

# ECF Distribution Provider - Generic
org.eclipse.ecf:org.eclipse.ecf.provider:4.9.1
org.eclipse.ecf:org.eclipse.ecf.provider.remoteservice:4.6.1

There are of course more artifacts provided by ECF. But for this example we keep the minimum needed.

Note:
Since the ECF artifacts are available on Maven Central you could also simply edit the existing central.maven file and add the ECF arteficts there, but for a better separation we split it here.

Now add the created ecfatcentral.maven file to the workspace build:

  • Open the cnf/build.bnd file
  • Add the following instruction
-plugin.10.ECFATCENTRAL: \
    aQute.bnd.repository.maven.provider.MavenBndRepository; \
        releaseUrl=https://repo.maven.apache.org/maven2/; \
        index=${.}/ecfatcentral.maven; \
        name="ECF Remote Services"

Bndtools also provides the option to include a p2 repository directly as explained here. To use the ECF p2 repository directly add the following instruction to the build.bnd file instead:

-plugin.11.p2: \
 	aQute.bnd.repository.p2.provider.P2Repository; \
 	url = https://download.eclipse.org/rt/ecf/3.14.31/site.p2; \
 	name = ECF Remote Services p2

Note:
If the newly added repositories do not show up in the Repositories view (bottom left in the default Bndtools Perspective), click on Reload workspace in the Bndtools Explorer (the circle arrows in the upper left corner).

Ensure that you have switched to the Bndtools Perspective for the following steps.

Service Interface

  • Create the Service API project
    • File -> New -> Bnd OSGi Project
    • Select the template
      • ECF Templates: Remote Service Project Templates -> Remote Service API Project
      • Bnd Templates: OSGi Release 7 Templates -> API Project
    • Click Next
    • Set name to org.fipro.modifier.api
    • Set JRE to JavaSE-11
    • Click Finish
  • On the New module-info.java dialog select Don’t Create Otherwise you will see compile errors as the OSGi annotations are not resolvable and you need to edit the module-info.java file to solve this. Or delete the module-info.java file.
  • Double check that the necessary export configurations are correctly specified via Bundle Annotations in the file package-info.java
  • Delete the created example files HelloService.java or ExampleConsumerInterface.java and ExampleProviderInterface.java
  • Copy the following interface StringModifier in the package org.fipro.modifier.api
package org.fipro.modifier.api;

public interface StringModifier {
    String modify(String input);
}

Service Implementation

  • Create the Service Implementation project
    • File -> New -> Bnd OSGi Project
    • Select the template
      • ECF Templates: Remote Service Project Templates -> Remote Service Impl Project
      • Bnd Templates: OSGi Release 7 Templates -> Component Development
    • Click Next
    • Set name to org.fipro.modifier.inverter
    • Set JRE to JavaSE-11
    • Using the ECF Template:
      • Click Next
      • Set api_package to org.fipro.modifier.api
      • Set service_exported_config to ecf.generic.server
    • Click Finish
  • On the New module-info.java dialog select Don’t Create
  • Delete the created example files HelloServiceImpl.java or Example.java
  • Using the Bnd Templates:
    • Open the file bnd.bnd
      • Switch to the Build tab
      • Add org.fipro.modifier.api to the Build Path via the green plus icon
      • Save
  • Copy the following class StringInverter into the package org.fipro.modifier.inverter
package org.fipro.modifier.inverter;

import org.fipro.modifier.api.StringModifier;
import org.osgi.service.component.annotations.Component;

@Component(property= {
    "service.exported.interfaces=*",
    "service.exported.configs=ecf.generic.server" }
)
public class StringInverter implements StringModifier {

    @Override
    public String modify(String input) {
        return (input != null)
            ? new StringBuilder(input).reverse().toString()
            : "No input given";
    }
}

The only thing that needs to be done additionally in comparison to creating a local OSGi service, is to configure that the service should be exported as Remote Service. This is done by setting the component property service.exported.interfaces. The value of this property needs to be a list of types for which the class is registered as a service. For a simple use case like the above, the asterisk can be used, which means to export the service for all interfaces under which it is registered, but to ignore the classes. For more detailed information have a look at the Remote Service Properties section of the OSGi Compendium Specification.

The other component property used in the above example is service.exported.configs. This property is used to specify the configuration types, for which the Distribution Provider should create Endpoints. If it is not specified, the Distribution Provider is free to choose the default configuration type for the service.

Note:
In the above example we use the ECF Generic Provider. This one by default chooses a SSL configuration type, so without additional configuration the example would not work if we don’t specify the configuration type.

Additionally you can specify Intents via the service.exported.intents component property to constrain the possible communication mechanisms that a distribution provider can choose to distribute a service. An example will be provided at a later step.

Service Provider Runtime

  • Create the Service Application project
    • File -> New -> Bnd OSGi Project
    • Select the template OSGi Release 7 Templates -> Application Project
    • Click Next
    • Set name to org.fipro.modifier.inverter.app
    • Set JRE to JavaSE-11
    • Click Finish
    • On the New module-info.java dialog select Don’t Create
  • Open the file org.fipro.modifier.inverter.app.bndrun
    • Add the following bundles to the Run Requirements
      • org.fipro.modifier.inverter
      • org.eclipse.ecf.osgi.services.distribution
      • org.eclipse.ecf.provider.jmdns
      • org.eclipse.ecf.provider.remoteservice
    • Remove the following bundle from the Run Requirements
      • org.fipro.modifier.inverter.app (for this example we do not add any configuration, so the bundle and its dependencies are not needed)
    • Set Execution Env.: JavaSE-11
  • Save the changes
  • Click on Resolve
  • Accept the result in the opening dialog via Update

Now you can start the org.fipro.modifier.inverter.app via the Run OSGi button in the upper right corner of the editor. With the console bundles in the Run Requirements the console will be available, apart from that you won’t see anything now.

Note:
The creation of a dedicated application project is not mandatory, but a recommended best practice to separate the application runtime from the service implementation. Especially if you consider that an application typically consists of several services, it doesn’t make much sense to have the launch configuration in one service bundle project. For this tutorial and for testing you can of course also edit the .bndrun file in the Service Implementation project.

Note:
If you used the ECF Project Templates to create the Service Implementation project, you will find two pre-configured .bndrun files in the project root that can be used to start the Service Provider Runtime. Open the file org.fipro.modifier.inverter.zeroconf.generic.bndrun and click on Resolve to calculate the Run Bundles. Once the result is accepted via Update in the dialog, the Service Provider Runtime can be started via Run OSGi.

Service Consumer

The implementation of a Remote Service Consumer also quite simple. From the development perspective there is nothing to consider. The service consumer is implemented without any additions. Only the runtime needs to be extended to contain the necessary bundles for Discovery and Distribution.

The simplest way of implementing a service consumer is a Gogo Shell command.

  • Create the Service Consumer project
    • File -> New -> Bnd OSGi Project
    • Select the template
      • ECF Templates: Remote Service Project Templates -> Remote Service Consumer Project
      • Bnd Templates: OSGi Release 7 Templates -> Component Development
    • Click Next
    • Set name to org.fipro.modifier.client
    • Set JRE to JavaSE-11
    • Using the ECF Template:
      • Click Next
      • Set api_package to org.fipro.modifier.api
    • Click Finish
  • On the New module-info.java dialog select Don’t Create
  • Using the Bnd Templates:
    • Open the file bnd.bnd
      • Switch to the Build tab
      • Add org.fipro.modifier.api to the Build Path via the green plus icon
      • Save
  • Delete the created files HelloServiceConsumer.java or Example.java
  • Copy the following class ModifyCommand into the package org.fipro.modifier.client
package org.fipro.modifier.client;

import java.util.List;

import org.fipro.modifier.api.StringModifier;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

@Component(
    property= {
        "osgi.command.scope:String=fipro",
        "osgi.command.function:String=modify"},
    service=ModifyCommand.class
)
public class ModifyCommand {

    @Reference
    volatile List<StringModifier> modifier;

    public void modify(String input) {
        if (modifier.isEmpty()) {
            System.out.println("No StringModifier registered");
        } else {
            modifier.forEach(m -> System.out.println(m.modify(input)));
        }
    }
}

Service Consumer Runtime

  • Create the Client Application project
    • File -> New -> Bnd OSGi Project
    • Select the template OSGi Release 7 Templates -> Application Project
    • Click Next
    • Set name to org.fipro.modifier.client.app
    • Set JRE to JavaSE-11
    • Click Finish
    • On the New module-info.java dialog select Don’t Create
  • Open the file org.fipro.modifier.client.app.bndrun
    • Add the following bundles to the Run Requirements
      • org.apache.felix.gogo.shell
      • org.apache.felix.gogo.command
      • org.fipro.modifier.client
      • org.eclipse.ecf.osgi.services.distribution
      • org.eclipse.ecf.provider.jmdns
      • org.eclipse.ecf.provider.remoteservice
      • org.eclipse.equinox.event
    • Remove the following bundle from the Run Requirements
      • org.fipro.modifier.client.app
    • Set Execution Env.: JavaSE-11
  • Save the changes
  • Click on Resolve
  • Accept the result in the opening dialog via Update

If you now click on Run OSGi on the Run tab of the editor, the Gogo Shell becomes available in the Console view of the IDE. Once the application is started you can execute the created Gogo Shell command via

modify <input>

If services are available, it will print out the modified results. Otherwise the message “No StringModifier registered” will be printed.

Note:
If you used the ECF Project Templates to create the Service Consumer project, you will find two pre-configured .bndrun files in the project root that can be used to start the Service Consumer Runtime. Open the file org.fipro.modifier.client.zeroconf.generic.bndrun and click on Resolve to calculate the Run Bundles. Once the result is accepted via Update in the dialog, the Service Provider Runtime can be started via Run OSGi.

Remote Service Admin Events

There are several events with regards to importing and exporting Remote Services, that are fired by the Remote Service Admin synchronously once they happen. These events are posted asynchronously via the OSGi Event Admin under the topic

org/osgi/service/remoteserviceadmin/<type>

Where <type> can be one of the following:

  • EXPORT_ERROR
  • EXPORT_REGISTRATION
  • EXPORT_UNREGISTRATION
  • EXPORT_UPDATE
  • EXPORT_WARNING
  • IMPORT_ERROR
  • IMPORT_REGISTRATION
  • IMPORT_UNREGISTRATION
  • IMPORT_UPDATE
  • IMPORT_WARNING

A simple event listener that prints to the console on any Remote Service Admin Event could look like this:

@Component(property = EventConstants.EVENT_TOPIC + "=org/osgi/service/remoteserviceadmin/*")
public class RemoteServiceEventListener implements EventHandler {

    @Override
    public void handleEvent(Event event) {
        System.out.println(event.getTopic());
        for (String objectClass :  ((String[])event.getProperty("objectClass"))) {
            System.out.println("\t"+objectClass);
        }
    }

}

For further details on the Remote Service Admin Events have a look at the OSGi Compendium Specification Chapter 122.7.

If you need to react synchronously on these events, you can implement a RemoteServiceAdminListener. I typically would not recommend this, unless you really want blocking calls on import/export events. Typically it is intended to be used internally by the Remote Service Admin. But for debugging purposes the ECF project also provides a DebugRemoteServiceAdminListener. It writes the endpoint description via a Writer to support debugging of Remote Services. Via the following class you could easily register a DebugRemoteServiceAdminListener via OSGi DS that prints the information on the console.

@Component
public class DebugListener
    extends DebugRemoteServiceAdminListener
    implements RemoteServiceAdminListener {
	// register the DebugRemoteServiceAdminListener via DS
}

To test this you can either add the above components to one of the existing bundles, or create a new bundle and add that bundle to the runtimes.

Runtime Debugging

The ECF project provides several ways for runtime inspection and runtime debugging. This is mainly done Gogo Shell commands provided via separate bundles. To enable the OSGi console and the ECF console commands, you need to add the following bundles to your runtime:

  • OSGi Console
    • org.apache.felix.gogo.command
    • org.apache.felix.gogo.runtime
    • org.apache.felix.gogo.shell
  • ECF Console
    • org.eclipse.ecf.console
    • org.eclipse.ecf.osgi.services.remoteserviceadmin.console

With the ECF Console bundles added to the runtime, there are several commands to inspect and interact with the Remote Service Admin. As an overview the available commands are listed in the wiki: Gogo Commands for Remote Services Development

Additionally the DebugRemoteServiceAdminListener described above is activated by default with the ECF Console bundles. It can be activated or deactivated in the runtime via the command

ecf:rsadebug <true/false>

JAX-RS Distribution Provider

One of the biggest issues I faced when working with Remote Services is networking as mentioned in the introduction. In the above example the ECF Generic Distribution Provider is used for a simpler setup. But for example in a corporate network with enabled firewalls somewhere in the network setup, the example will probably not work. As said before, the ECF project provides multiple Distribution Provider implementations, which gives the opportunity to configure the setup to match the project needs. One interesting implementation in that area is the JAX-RS Distribution Provider. Using that one could probably help solving several of the networking issues related to firewalls. But as with the whole Remote Service topic, the complexity in the setup is quite high because of the increased number of dependencies that need to be resolved.

The JAX-RS Distribution Provider implementation is available for Eclipse Jersey and Apache CXF. It uses the OSGi HttpService to register the JAX-RS resource, and of course it then also needs a Servlet container like Eclipse Jetty to provide the JAX-RS resource. I will show the usage of the Jersey based implementation in the following sections.

Project Setup

Unfortunately the JAX-RS Distribution Provider is not available via Maven Central. As Bndtools supports p2 Repositories, we can add the one from GitHub to make it available in our workspace. The p2 support is only able to add the whole repository, so you will see everything from that p2 repository in the workspace. But as there is no availability on Maven Central, the only other option would be to download the artifacts locally and place them in a local structure (which is actually what the ECF Bndtools Workspace Template does). If you used the ECF Bndtools Workspace Templates, the JAX-RS Distribution Provider and its dependencies are already available in the workspace. There are no additional steps necessary for consuming the JAX-RS Distribution Provider and its dependencies.

If you have chosen the manual project setup I recommend to use the p2 repository:

  • Open the cnf/build.bnd file
    • Switch to the Source tab
    • Add the following instruction to add the JAX-RS Distribution Provider p2 repository
-plugin.12.p2: \
 	aQute.bnd.repository.p2.provider.P2Repository; \
 	url = https://raw.githubusercontent.com/ECF/JaxRSProviders/master/build/; \
 	name = ECF JAX-RS Distribution Provider p2

Additionally we need a server that publishes the JAX-RS resource. We will use a Jetty server.

  • Open the cnf/central.maven file
    • Add the following GAV coordinates to that file
org.apache.felix:org.apache.felix.http.jetty:4.1.14

Note:
The ECF Bndtools Workspace Template used a local repository approach in the past. That means the artifacts are physically located in subfolders of the cnf directory. To update them you needed to download the artifacts from the respective GitHub repositories and add/replace the jars in the local repository structure. This was recently changed to also make use of the p2 repository support. If you created an ECF Bndtools Workspace in the past you might want to check if the usage of p2 repositories could improve your project setup.

Note:
The local repository approach and the limitation with regards to updates can also be seen as an advantage. The JAX-RS Distribution Provider is not yet released and published officially. So the p2 update site is generic and if the libraries are updated there, the updates will be directly consumed on a workspace update. Anyhow I personally don’t like having jars locally in my Bnd OSGi Workspace as these artifacts also need to be checked into the repository. I’d rather configure the remote repositories and go into the “offline mode” in case I have to work without an internet connection.

JAX-RS Remote Service Implementation

The implementation of the service already looks different compared to what you have seen so far. Instead of only adding the necessary Component Properties to configure the service as a Remote Service, the service implementation does directly contain the JAX-RS annotations. That of course also means that the annotations need to be available.

  • Create the Service Implementation project
    • File -> New -> Bnd OSGi Project
    • Select the template
      • ECF Templates: Remote Service Project Templates -> JaxRS Remote Service Hello Impl Project
      • Bnd Templates: OSGi Release 7 Templates -> Component Development
    • Click Next
    • Set name to org.fipro.modifier.uppercase
    • Set JRE to JavaSE-11
    • Using the ECF Template:
      • Click Next
      • Set api_package to org.fipro.modifier.api
    • Click Finish
    • On the New module-info.java dialog select Don’t Create
  • Delete the file HelloWorldResource.java or Example.java
  • Using the Bnd Templates
    • Open the file bnd.bnd
      • Switch to the Build tab
      • Add the following bundles to the Build Path via the green plus icon
        • org.fipro.modifier.api
        • jakarta.ws.rs-api
      • Save
  • Copy the following UppercaseModifier snippet the project
package org.fipro.modifier.uppercase;

import java.util.Locale;

import org.fipro.modifier.api.StringModifier;
import org.osgi.service.component.annotations.Component;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

//The JAX-RS path annotation for this service
@Path("/modify")
//The OSGi DS component annotation
@Component(
    immediate = true,
    property = {
        "service.exported.interfaces=*",
        "service.exported.intents=jaxrs"})
public class UppercaseModifier implements StringModifier {

    @GET
    // The JAX-RS annotation to specify the result type
    @Produces(MediaType.TEXT_PLAIN)
    // The JAX-RS annotation to specify that the last part
    // of the URL is used as method parameter
    @Path("/{value}")
    @Override
    public String modify(@PathParam("value") String input) {
        return (input != null)
            ? input.toUpperCase(Locale.getDefault())
            : "No input given";
    }
}

For the JAX-RS annotations, please have a look at various existing tutorials and blog posts in the internet, for example

About the OSGi DS configuration:

  • The service is an Immediate Compontent, so it is consumed by the OSGi Http Whiteboard on startup
  • Export all interfaces as Remote Service via **service.exported.interfaces=***
  • Configure that JAX-RS is used as communication mechanism by the distribution provider via **service.exported.intents=jaxrs**

Note:
As mentioned earlier there is a bug in ECF 3.14.26 which is integrated in the Eclipse 2021-21 SimRel repo. The service.exported.intents property is not enough to get the JAX-RS resource registered. Additionally it is necessary to set service.exported.configs=ecf.jaxrs.jersey.server to make it work. This was fixed shortly after I reported it and is included with the current ECF 3.14.31 release. The basic idea of the intent configuration is to make the service independent of the underlying JAX-RS Distribution Provider implementation (Jersey vs. Apache CXF).

JAX-RS Jersey Distribution Provider Dependencies

For the JAX-RS Distribution Provider Runtime a lot more dependencies are required. The following list should cover the additional necessary base dependencies:

  • Jackson
    • com.fasterxml.jackson.core.jackson-annotations
    • com.fasterxml.jackson.core.jackson-core
    • com.fasterxml.jackson.core.jackson-databind
    • com.fasterxml.jackson.jaxrs.jackson-jaxrs-base
    • com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider
    • com.fasterxml.jackson.module.jackson-module-jaxb-annotations
  • Jersey / Glassfish / Dependencies
    • org.glassfish.hk2.api
    • org.glassfish.hk2.external.aopalliance-repackaged
    • org.glassfish.hk2.external.jakarta.inject
    • org.glassfish.hk2.locator
    • org.glassfish.hk2.osgi-resource-locator
    • org.glassfish.hk2.utils
    • org.glassfish.jersey.containers.jersey-container-servlet
    • org.glassfish.jersey.containers.jersey-container-servlet-core
    • org.glassfish.jersey.core.jersey-client
    • org.glassfish.jersey.core.jersey-common
    • org.glassfish.jersey.core.jersey-server
    • org.glassfish.jersey.ext.jersey-entity-filtering
    • org.glassfish.jersey.inject.jersey-hk2
    • org.glassfish.jersey.media.jersey-media-jaxb
    • org.glassfish.jersey.media.jersey-media-json-jackson
    • com.sun.activation.javax.activation
    • jakarta.annotation-api
    • javax.ws.rs-api
    • jakarta.xml.bind-api
    • javassist
    • javax.validation.api
    • org.slf4j.api

For the Service Provider we need the following dependencies, which are the JAX-RS Jersey Distribution Provider Server bundles, the Jetty as embedded server and the HTTP Whiteboard:

  • ECF Distribution Provider - JAX-RS Jersey
    • org.eclipse.ecf.provider.jaxrs
    • org.eclipse.ecf.provider.jaxrs.server
    • org.eclipse.ecf.provider.jersey.server
  • Jetty / Http Whiteboard / Http Service
    • org.apache.felix.http.jetty
    • org.apache.felix.http.servlet-api

For the Service Consumer we need the following dependencies, which are the JAX-RS Jersey Distribution Provider Client bundles and the HttpClient to be able to access the JAX-RS resource:

  • ECF Distribution Provider - JAX-RS Jersey
    • org.eclipse.ecf.provider.jaxrs
    • org.eclipse.ecf.provider.jaxrs.client
    • org.eclipse.ecf.provider.jersey.client

Service Provider Runtime

  • Create the Service Application project
    • File -> New -> Bnd OSGi Project
    • Select the template OSGi Release 7 Templates -> Application Project
    • Click Next
    • Set name to org.fipro.modifier.uppercase.app
    • Set JRE to JavaSE-11
    • Click Finish
    • On the New module-info.java dialog select Don’t Create
  • Open the file org.fipro.modifier.uppercase.app.bndrun
    • Add the following bundles to the Run Requirements
      • org.fipro.modifier.uppercase
      • org.eclipse.ecf.osgi.services.distribution
      • org.eclipse.ecf.provider.jmdns
      • org.eclipse.ecf.provider.jersey.server
      • org.apache.felix.http.jetty
      • org.eclipse.equinox.event
    • Remove the following bundle from the Run Requirements
      • org.fipro.modifier.uppercase.app
    • Optional: Add the following console bundles for debugging and inspection
      • org.apache.felix.gogo.command
      • org.apache.felix.gogo.shell
      • org.eclipse.ecf.osgi.services.remoteserviceadmin.console
    • Set Execution Env.: JavaSE-11
    • Add the following property to the OSGi Framework properties:
      • org.osgi.service.http.port=8181
    • Save the changes
    • Click on Resolve
    • Accept the result in the opening dialog via Update

Note:
With the latest version of the JAX-RS Distribution Provider, the .bndrun configuration is much more comfortable than before. There were several improvements to make the definition of a runtime more user friendly, so if you are already familiar with the JAX-RS Distribution Provider and used it in the past, be sure to update it to the latest version to benefit from the latest modifications.

Now you can start the Uppercase JAX-RS Service Runtime from the Overview tab via Launch an Eclipse application. After the runtime is started the service will be available as JAX-RS resource and can be accessed in a browser, e.g. http://localhost:8181/modify/remoteservice

Note:
Unfortunately with the above setup, you will see a 404 instead of the service result. It seems that using Jetty 9 the usage of the base URL is not working for Remote Services. Maybe it is only a configuration issue that I was not able to solve as part of this tutorial. There are two options to handle this issue, either configure additional path segments or use Jetty 10.

Note:
Don’t worry if you see a SelectContainerException in the console. It is only an information that tells that the service from the first part of the tutorial can not be imported in the runtime of this part of the tutorial and vice versa. The first service is distributed via the Generic Provider, while the second service is distributed by the JAX-RS Provider. But both are using the JmDNS Discovery Provider.

The URL path is defined via the JAX-RS annotations, “modify” via @Path("/modify") on the class, “remoteservice” is the path parameter defined via @Path("/{value}") on the method (if you change that value, the result will change accordingly). You can extend the URL via configurations shown below:

  • Add a prefix URL path segment on runtime level:
    Add the following property to the OSGi Framework properties
    ecf.jaxrs.server.pathPrefix=<value>
    (e.g. ecf.jaxrs.server.pathPrefix=/services)
  • Add a leading URL path segment on service level:
    Add the following component property to the @Component annotation
    ecf.jaxrs.server.pathPrefix=<value> e.g.
    @Component(
      immediate = true,
      property = {
          "service.exported.interfaces=\*",
          "service.exported.intents=jaxrs",
          "ecf.jaxrs.server.pathPrefix=/upper"})
    

If all of the above configurations are added, the new URL to the service is, e.g. http://localhost:8181/services/upper/modify/remoteservice

Additional information about available component properties can be found here: Jersey Service Properties

Service Provider Runtime - Jetty 10

With the above setup the bundle org.apache.felix.http.jetty is integrated in the runtime. That bundle combines the following:

  • OSGi Http Service
  • OSGi Http Whiteboard
  • Jetty 9

This makes the integration very easy. If you want to update to Jetty 10 the setup is more complicated, as that is not available as combined Felix bundle. In that case you need the following bundles:

  • Jetty 10
    • org.eclipse.jetty.http
    • org.eclipse.jetty.io
    • org.eclipse.jetty.security
    • org.eclipse.jetty.server
    • org.eclipse.jetty.servlet
    • org.eclipse.jetty.util
    • org.eclipse.jetty.util.ajax
  • OSGi Http Service and Http Whiteboard (Equinox / Jetty)
    • org.eclipse.equinox.http.jetty
    • org.eclipse.equinox.http.servlet
  • OSGi Service Interfaces
    • org.eclipse.osgi.services

First you need to add the necessary artifacts to the workspace:

  • Open the cnf/central.maven file
    • Add the following GAV coordinates to that file
org.eclipse.platform:org.eclipse.osgi.services:jar:3.10.200
org.eclipse.platform:org.eclipse.equinox.http.jetty:jar:3.8.100
org.eclipse.platform:org.eclipse.equinox.http.servlet:jar:1.7.200

org.eclipse.jetty:jetty-http:jar:10.0.8
org.eclipse.jetty:jetty-io:jar:10.0.8
org.eclipse.jetty:jetty-security:jar:10.0.8
org.eclipse.jetty:jetty-server:jar:10.0.8
org.eclipse.jetty:jetty-servlet:jar:10.0.8
org.eclipse.jetty:jetty-util:jar:10.0.8
org.eclipse.jetty:jetty-util-ajax:jar:10.0.8

jakarta.servlet:jakarta.servlet-api:jar:4.0.4

After that you can create a new Service Provider Runtime project that includes Jetty 10:

  • Create the Service Application project
    • File -> New -> Bnd OSGi Project
    • Select the template OSGi Release 7 Templates -> Application Project
    • Click Next
    • Set name to org.fipro.modifier.uppercase.app.jetty10
    • Set JRE to JavaSE-11
    • Click Finish
    • On the New module-info.java dialog select Don’t Create
  • Open the file org.fipro.modifier.uppercase.app.jetty10.bndrun
    • Add the following bundles to the Run Requirements
      • org.fipro.modifier.uppercase
      • org.eclipse.ecf.osgi.services.distribution
      • org.eclipse.ecf.provider.jmdns
      • org.eclipse.ecf.provider.jersey.server
      • org.eclipse.equinox.http.jetty
      • org.eclipse.equinox.http.servlet
      • org.eclipse.jetty.http
      • org.eclipse.jetty.io
      • org.eclipse.jetty.security
      • org.eclipse.jetty.server
      • org.eclipse.jetty.servlet
      • org.eclipse.jetty.util
      • org.eclipse.jetty.util.ajax
      • org.eclipse.equinox.event
    • Remove the following bundle from the Run Requirements
      • org.fipro.modifier.uppercase.app.jetty10
    • Add org.apache.felix.http.jetty to the Run Blacklist (this is necessary to avoid that this bundle is used by the resolve step)
    • Optional: Add the following console bundles for debugging and inspection
      • org.apache.felix.gogo.command
      • org.apache.felix.gogo.shell
      • org.eclipse.ecf.osgi.services.remoteserviceadmin.console
    • Set Execution Env.: JavaSE-11
    • Add the following property to the OSGi Framework properties:
      • org.osgi.service.http.port=8181
      • launch.activation.eager=true
    • Save the changes
    • Click on Resolve
    • Accept the result in the opening dialog via Update

Note:
The OSGi Framework property launch.activation.eager=true is necessary because of the activation policy set in the Equinox Jetty Http Service bundle. It is configured to be activated lazy, which means it will only be activated if someone requests something from that bundle. But as Equinox does collect all OSGi service interfaces in org.eclipse.osgi.services, actually nobody ever will request something from that bundle, which leaves it in the STARTING state forever. With launch.activation.eager property the lazy activation will be ignored and all bundles will be simply started. Bug 530076 was created to discuss if the lazy activation could be dropped.

Note:
Unfortunately you can not include the org.apache.felix.webconsole in a Jetty 10 runtime. The reason is the Servlet API version dependency of webconsole. org.apache.felix.webconsole requires javax.servlet;version="[2.4,4)" even in its latest version, while org.eclipse.jetty.servlet requires javax.servlet;version="[4.0.0,5)". So if you want to use the webconsole in your JAX-RS Remote Service, you need to stick with Jetty 9.

Note:
It is currently not possible to use Jetty 11 for OSGi development, as the OSGi implementations are not updated to the jakarta namespace.

For an overview on the Jetty versions and dependencies, have a look at the Jetty Downloads page.

Service Consumer Runtime

To consume the Remote Service provided via JAX-RS Distribution Provider, the runtime needs to be extended to include the additional dependencies:

  • Open the file org.fipro.modifier.client.app.bndrun
    • Add the following bundle to the Run Requirements
      • org.eclipse.ecf.provider.jersey.client
    • Save the changes
    • Click on Resolve to update the Run Bundles

If you now start the Service Consumer Runtime and have the Service Provider Runtime also running, you can execute the following command

modify jax

This will actually lead to an error if you followed my tutorial step by step:

ServiceException: Service exception on remote service proxy

The reason is that the Service Interface does not contain the JAX-RS annotations as the service actually does, and therefore the mapping is working. So while for providing the service the interface does not need to be modified, it has to for the consumer side.

Extend the Service Interface

  • Open the file org.fipro.modifier.api/bnd.bnd
  • Switch to the Build tab
    • Add the following bundle to the Build Path via the green plus icon
      • jakarta.ws.rs-api
  • Open the StringModifier class and add the JAX-RS annotations to be exactly the same as for the Service Implementation
package org.fipro.modifier.api;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/modify")
public interface StringModifier {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{value}")
    String modify(@PathParam("value") String input);
}

If you now start the Uppercase Service Provider Runtime and the Service Consumer Runtime again, the error should be gone and you should see the expected result.

Update the “Inverter” Service Provider Runtime

After the Service Interface was extended to include the JAX-RS annotations, the first Service Provider Runtime will not resolve anymore because of missing dependencies. To fix this:

  • Open the file org.fipro.modifier.inverter.app.bndrun
    • Click on Resolve to update the Run Bundles

Now you can start that Service Provider Runtime again. If the other Service Provider and the Service Consumer are also active, executing the modify command will now output the result of both services.

Endpoint Description Extender Format (EDEF)

In the tutorial we used JmDNS/Zeroconf as Discovery Provider. This way there is not much we have to do as a developer or administrator despite adding the according bundle to the runtime. This kind of Discovery is using a broadcast mechanism to announce the service in the network. In cases this doesn’t work, e.g. firewall rules that block broadcasting, it is also possible that you use a static file-based discovery. This can be done using the Endpoint Description Extender Format (EDEF) and is also supported by ECF.

Let’s create an additional service that is distributed via JAX-RS. But this time we exclude the org.eclipse.ecf.provider.jmdns bundle, so there is no additional discovery inside the Service Provider Runtime. We also add the console bundles to be able to inspect the runtime.

Note:
If you don’t want to create another service, you can also modify the previous uppercase service. In that case remove the org.eclipse.ecf.provider.jmdns bundle from the product configuration and ensure that the console bundles are added to be able to inspect the remote service runtime via the OSGi Console.

  • Create the Service Implementation project
    • File -> New -> Bnd OSGi Project
    • Select the template
      • ECF Templates: Remote Service Project Templates -> JaxRS Remote Service Hello Impl Project
      • Bnd Templates: OSGi Release 7 Templates -> Component Development
    • Click Next
    • Set name to org.fipro.modifier.camelcase
    • Set JRE to JavaSE-11
    • Using the ECF Template:
      • Click Next
      • Set api_package to org.fipro.modifier.api
    • Click Finish
    • On the New module-info.java dialog select Don’t Create
  • Delete the file HelloWorldResource.java or Example.java
  • Using the Bnd Templates
    • Open the file bnd.bnd
      • Switch to the Build tab
      • Add the following bundles to the Build Path via the green plus icon
        • org.fipro.modifier.api
        • jakarta.ws.rs-api
      • Save
  • Copy the following CamelCaseModifier snippet the project
package org.fipro.modifier.camelcase;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.fipro.modifier.api.StringModifier;
import org.osgi.service.component.annotations.Component;

@Path("/modify")
@Component(
    immediate = true,
    property = {
        "service.exported.interfaces=*",
        "service.exported.intents=jaxrs",
        "ecf.jaxrs.server.pathPrefix=/camelcase"})
public class CamelCaseModifier implements StringModifier {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/{value}")
    @Override
    public String modify(@PathParam("value") String input) {
        StringBuilder builder = new StringBuilder();
        if (input != null) {
            for (int i = 0; i < input.length(); i++) {
                char currentChar = input.charAt(i);
                if (i % 2 == 0) {
                    builder.append(Character.toUpperCase(currentChar));
                } else {
                    builder.append(Character.toLowerCase(currentChar));
                }
            }
        }
        else {
            builder.append("No input given");
        }
        return builder.toString();
    }
}
  • Create the Service Application project
    • File -> New -> Bnd OSGi Project
    • Select the template OSGi Release 7 Templates -> Application Project
    • Click Next
    • Set name to org.fipro.modifier.camelcase.app
    • Set JRE to JavaSE-11
    • Click Finish
    • On the New module-info.java dialog select Don’t Create
  • Open the file org.fipro.modifier.camelcase.app.bndrun
    • Add the following bundles to the Run Requirements
      • org.apache.felix.gogo.shell
      • org.apache.felix.gogo.command
      • org.fipro.modifier.camelcase
      • org.eclipse.ecf.osgi.services.distribution
      • org.eclipse.ecf.provider.jersey.server
      • org.apache.felix.http.jetty
      • org.eclipse.ecf.osgi.services.remoteserviceadmin.console
    • Remove the following bundle from the Run Requirements
      • org.fipro.modifier.camelcase.app
    • Set Execution Env.: JavaSE-11
    • Add the following properties to the OSGi Framework properties:
      • org.osgi.service.http.port=8282
      • ecf.jaxrs.server.pathPrefix=/services
    • Save the changes
    • Click on Resolve
    • Accept the result in the opening dialog via Update

Once the runtime is started via Run OSGi the service should be available via http://localhost:8282/services/camelcase/modify/remoteservice

You probably noticed a console output on startup that shows the Endpoint Description XML. This is actually what we need for the EDEF file. You can also get the endpoint description at runtime via the ECF Gogo Command listexports <endpoint.id>:

osgi> listexports
endpoint.id                          |Exporting Container ID                       |Exported Service Id
5918da3a-a971-429f-9ff6-87abc70d4742 |http://localhost:8282/services/camelcase     |38

osgi> listexports 5918da3a-a971-429f-9ff6-87abc70d4742
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
  <endpoint-description>
    <property name="ecf.endpoint.id" value-type="String" value="http://localhost:8282/services/camelcase"/>
    <property name="ecf.endpoint.id.ns" value-type="String" value="ecf.namespace.jaxrs"/>
    <property name="ecf.endpoint.ts" value-type="Long" value="1642667915518"/>
    <property name="ecf.jaxrs.server.pathPrefix" value-type="String" value="/camelcase"/>
    <property name="ecf.rsvc.id" value-type="Long" value="1"/>
    <property name="endpoint.framework.uuid" value-type="String" value="80778aff-63c7-448d-92a5-7902eb6782ae"/>
    <property name="endpoint.id" value-type="String" value="5918da3a-a971-429f-9ff6-87abc70d4742"/>
    <property name="endpoint.package.version.org.fipro.modifier" value-type="String" value="1.0.0"/>
    <property name="endpoint.service.id" value-type="Long" value="38"/>
    <property name="objectClass" value-type="String">
      <array>
        <value>org.fipro.modifier.StringModifier</value>
      </array>
    </property>
    <property name="remote.configs.supported" value-type="String">
      <array>
        <value>ecf.jaxrs.jersey.server</value>
      </array>
    </property>
    <property name="remote.intents.supported" value-type="String">
      <array>
        <value>passByValue</value>
        <value>exactlyOnce</value>
        <value>ordered</value>
        <value>osgi.async</value>
        <value>osgi.private</value>
        <value>osgi.confidential</value>
        <value>jaxrs</value>
      </array>
    </property>
    <property name="service.imported" value-type="String" value="true"/>
    <property name="service.imported.configs" value-type="String">
      <array>
        <value>ecf.jaxrs.jersey.server</value>
      </array>
    </property>
    <property name="service.intents" value-type="String">
      <array>
        <value>jaxrs</value>
      </array>
    </property>
  </endpoint-description>
</endpoint-descriptions>

The endpoint description is needed by the Service Consumer to discover the new service. Without a Discovery that is broadcasting, the service needs to be discovered statically via an EDEF file. As the EDEF file is registered via manifest header, we create a new bundle. You could also place it in an existing bundle like org.fipro.modifier.client, but for some more OSGi dynamics fun, let’s create a new bundle.

  • Create the EDEF configuration bundle project
    • File -> New -> Bnd OSGi Project
    • Select the template Bndtools -> Empty
    • Click Next
    • Set name to org.fipro.modifier.client.edef
    • Set JRE to JavaSE-11
    • Click Finish
  • On the New module-info.java dialog select Don’t Create
  • Create a new folder edef
    • Right click on the project -> New -> Folder
    • Set Folder name to edef
    • Click Finish
  • Create a new file camelcase.xml in that folder
    • Right click on the edef folder -> New -> File
    • Set File name to camelcase.xml
  • Copy the Endpoint Description XML from the previous console command execution into that file
  • Open the bnd.bnd file
    • Switch to the Source tab
    • Add the following statements
-includeresource: edef=edef

Remote-Service: edef/camelcase.xml
  • Open the file org.fipro.modifier.client.app.bndrun
    • Add org.fipro.modifier.client.edef to the Run Requirements
    • Save the changes
    • Click on Resolve to update the Run Bundles

If you start the Service Consumer Runtime, the service will directly be available. This is because the new org.fipro.modifier.client.edef bundle is activated automatically by the bnd launcher (a big difference compared to Equinox). Let’s deactivate it via the console. First we need to find the bundle-id via lb and then stop it via stop <bundle-id>. The output should look similar to the following snippet:

g! lb edef
START LEVEL 1
   ID|State      |Level|Name
   50|Active     |    1|org.fipro.modifier.client.edef (0.0.0)|0.0.0

g! stop 50

Now the service becomes unavailable via the modify command. If you start the bundle, the service becomes available again.

ECF Extensions to EDEF

The EDEF specification itself would not be sufficient for productive usage. For example, the values of the endpoint description properties need to match. For the endpoint.id this would be really problematic, as that value is a random generated uuid and changes on each runtime start. So if the Service Provider Runtime is restarted there is a new endpoint.id value. ECF includes a mechanism to support the discovery and the distribution even if the endpoint.id of the importer and the exporter do not match. This actually makes the EDEF file support work in productive environments.

ECF also provides a mechanism to create an endpoint description using a properties file. All the necessary endpoint description properties need to be included as properties with the respective types and values. The following example shows the properties representation for the EDEF XML of the above example. Note that for endpoint.id and endpoint.framework.uuid the type is set to uuid and the value is 0. This way ECF will generate a random UUID and the matching feature will ensure that the distribution will work even without matching id values.

ecf.endpoint.id=http://localhost:8282/services/camelcase
ecf.endpoint.id.ns=ecf.namespace.jaxrs
ecf.endpoint.ts:Long=1642761763599
ecf.jaxrs.server.pathPrefix=/camelcase
ecf.rsvc.id:Long=1
endpoint.framework.uuid:uuid=0
endpoint.id:uuid=0
endpoint.package.version.org.fipro.modifier.api=1.0.0
endpoint.service.id:Long=38
objectClass:array=org.fipro.modifier.api.StringModifier
remote.configs.supported:array=ecf.jaxrs.jersey.server
remote.intents.supported:array=passByValue,exactlyOnce,ordered,osgi.async,osgi.private,osgi.confidential,jaxrs
service.imported:boolean=true
service.imported.configs:array=ecf.jaxrs.jersey.server
service.intents:array=jaxrs

Properties files can be used to override values in an underlying XML EDEF file and even as an alternative, so the XML file is not needed anymore. It is even possible to override properties values for different environments, which makes it very interesting in a productive environment. So there can be a default Properties file for the basic endpoint description, then an endpoint description per service that derives from the basic settings, and even profile specific settings that changes for example the ecf.endpoint.id URLs per profile (DEV/INT/PROD). More details on that topic can be found in the ECF Wiki.

Alternatively you can also trigger a remote service import via EDEF programmatically using classes from the org.osgi.service.remoteserviceadmin package (see below). This way it is possible to dynamically import and close remote service registrations at runtime (without operating via low level OSGi bundle operations). The following snippet is an example for the programmatic registration of the service above:

Map<String, Object> properties = new HashMap<>();

properties.put("ecf.endpoint.id", "http://localhost:8282/services/camelcase");
properties.put("ecf.endpoint.id.ns", "ecf.namespace.jaxrs");
properties.put("ecf.endpoint.ts", 1642489801532l);
properties.put("ecf.jaxrs.server.pathPrefix", "/camelcase");
properties.put("ecf.rsvc.id", 1l);
properties.put("endpoint.framework.uuid", "0");
properties.put("endpoint.id", "0");
properties.put("endpoint.package.version.org.fipro.modifier.api", "1.0.0");
properties.put("endpoint.service.id", 38l);
properties.put("objectClass", new String[] { "org.fipro.modifier.api.StringModifier" });
properties.put("remote.configs.supported", new String[] { "ecf.jaxrs.jersey.server" });
properties.put("remote.intents.supported", new String[] { "passByValue", "exactlyOnce", "ordered", "osgi.async", "osgi.private", "osgi.confidential", "jaxrs" });
properties.put("service.imported", "true");
properties.put("service.intents", new String[] { "jaxrs" });
properties.put("service.imported.configs", new String[] { "ecf.jaxrs.jersey.server" });

EndpointDescription desc = new EndpointDescription(properties);
ImportRegistration importRegistration = admin.importService(desc);

Conclusion

The OSGi specification has several chapters and implementations to support a microservice architecture. The Remote Service and Remote Service Admin specifications are one of these and probably the most complicated ones, which was confirmed by several OSGi experts I talked with at conferences. Also the specification itself is not easy to understand, but I hope that this blog post helps to get a better understanding.

While Remote Services are pretty easy to implement, the complicated steps are in the setup of the runtime by collecting all necessary bundles. While the ECF project provides several examples and also tries to provide support for better bundle resolving, it is still not a trivial task. I hope this tutorial helps also in solving that topic a little bit.

Of course at runtime you might face networking issues, as I did in every talk for example. The typical fallacies are even referred in the Remote Service Specification. With the usage of JAX-RS and HTTP for the distribution of services and EDEF for a static file-based discovery, this might be less problematic. Give them a try if you are running into troubles.

At the end I again want to thank Scott Lewis for his continuous work on ECF and his support whenever I faced issues with my examples and had questions on some details. If you need an extension or if you have other requests regarding ECF or the JAX-RS Distribution Provider, please get in touch with him.

References

Updated: