Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "Tutorial: Creating Custom Distribution Providers"

(Introduction)
Line 78: Line 78:
 
The RSA specification defines two major subsystems:  discovery and distribution.  Discovery concerns finding remote services exported by other processes on the network.  The distribution subsystem is responsible for the actual communication of invoking a remote call:  serializing remote method parameters, communicating with the remote service host via some network transport protocol, unmarshalling and invoking the service method with provided parameters, and returning a result to the consumer.
 
The RSA specification defines two major subsystems:  discovery and distribution.  Discovery concerns finding remote services exported by other processes on the network.  The distribution subsystem is responsible for the actual communication of invoking a remote call:  serializing remote method parameters, communicating with the remote service host via some network transport protocol, unmarshalling and invoking the service method with provided parameters, and returning a result to the consumer.
  
Rather than provide a single distribution implementation, ECF's implementation of RSA defines an abstract API that allows distribution providers to be created and used to implement the remote invocation.  This API is declared in the [[Distribution_Providers#Remote_Services_API | ECF remote services API]], provided by the org.eclipse.ecf.remoteservices bundle.  Custom distribution providers can simply implement a part of this API, and then at runtime they will be used to provide the functions of the distribution provider.
+
ECF's implementation of RSA defines an abstract API that allows distribution providers to be easily created and used to implement remote invocation.  This ECF API is declared in the [[Distribution_Providers#Remote_Services_API | ECF remote services API]], provided by the org.eclipse.ecf.remoteservices bundle.  Custom distribution providers simply implement needed part of this API, and then at runtime they will be used to provide the functions of the distribution provider.
  
This tutorial will walk through the creation of a simple custom distribution provider.  Although there are many existing [[Distribution_Providers | distribution providers]], there will continue to be new protocols (e.g. MQTT), new architectural communication styles (e.g. REST), and new serialization formats (e.g. JSON, or Protocol Buffers), as well as application or system-specific requirements for security, interoperability, and integration.  ECF's pluggable provider approach allows the easy creation of arbitrary distribution providers.
+
This tutorial will show the creation of a simple custom distribution provider.  Although there are existing [[Distribution_Providers | distribution providers]], there will continue to be new protocols, new communication styles, and new serialization formats (e.g. JSON, or Protocol Buffers), as well as application or system-specific requirements for security, interoperability, and integration.  ECF's pluggable provider approach allows the easy creation of distribution providers that meet these requirements without having to be concerned with implementing all of the OSGi specification requirements.
  
 +
==Remote Service Containers, IDs, and Namespaces===
  
 +
ECF has the concept of a 'container' ([http://download.eclipse.org/rt/ecf/3.13.0/javadoc/org/eclipse/ecf/core/IContainer.html IContainer]), which is an object that implements the remote services API, provides a grouping of remote services, and represents a network-accessible endpoint.
  
It's currently popular to create REST-based networked services.  This is understandable, as the ubiquity http/https, the simplicity of the REST approach, and the availability of open, cross-language object serialization formats like JSON and XML, along with the availability of quality distribution frameworks all make it easier than ever to define, implement, and deploy a service and it's associated API.
+
Containers have unique transport-specific [http://download.eclipse.org/rt/ecf/3.13.0/javadoc/org/eclipse/ecf/core/identity/ID.html ID].  Some examples of transport-specific container IDs:<br>
  
One thing that is relatively new, however, is that there are now open standards that deal with two levels of concerns
+
https://myhost.com/v1/path/to/service<br>
 +
ecftcp://localhost:3282/server<br>
 +
mqtt://mybroker.com/mytopic<br>
 +
jms://jmsbroker.com:6686/jmstopic<br>
 +
r_osgi://somehost/<br>
 +
hazelcast:///mytopicname
  
#Transport/Distribution-Level Concerns:  What protocol and serialization format are to be used?  How to support cross-language type systems?  How to be as bandwidth efficient and performant as possible?  How to handle network failure? etc.
+
Each ID must be unique within a [http://download.eclipse.org/rt/ecf/latest/javadoc/org/eclipse/ecf/core/identity/Namespace.html Namespace]. 
#Service-Level Concerns:  How to discover the service?  How to version the service?  How to handle service dynamics?  How to secure the service?  How to describe and document the service so that others may easily understand and use it?  How to combine services and have service-level dependencies in a dynamic environment? etc.
+
  
==Transport and Distribution Concerns==
+
==Implementing a Namespace for a Distribution Provider==
  
From the service implementer's perspective, the selection of a framework or implementation makes de-facto design choices about transport or distribution concerns.  For example, some REST frameworks use XML, some JSON, some support both.  However, once such a framework is chosen the service implemented and deployed, it can become very difficult, time-consuming, or even technically impossible to change to use other frameworks or distribution systems. 
+
The first thing a distribution provider must do is to register a new type of NamespaceECF provides a number of Namespace classes that can be extended to make this easy. For example, the URIIDNamespace can handle any ID syntax that can be represented as a URI, like all of the aboveHere is an example Namespace class that extends the URIIDNamespace:
 
+
For REST-based services, however, standards are beginning to emerge that allow REST services to be defined and implemented without making a specific choice of distribution system or framework.  One example in Java is [https://en.wikipedia.org/wiki/Java_API_for_RESTful_Web_Services JAX Remote Services] or JAX-RSJAX-RS defines/standardizes Java annotations for classes and methods that are to be exposed for remote access.   JAX-RS implementations then use these annotations to 'export' the service (make it available for remote access) and handle the mapping between http/https requests, and the appropriate method invocationFor example, here's a small 'hello-world' service that uses JAX-RS annotations:
+
  
 
<source lang="java">
 
<source lang="java">
import javax.ws.rs.GET;
+
package org.eclipse.ecf.example1.provider.dist.common;
import javax.ws.rs.Produces;
+
import javax.ws.rs.Path;
+
  
// The Java class will be hosted at the URI path "/helloworld"
+
import org.eclipse.ecf.core.identity.URIID.URIIDNamespace;
@Path("/helloworld")
+
 
public class HelloWorldResource implements HelloWorldService {
+
// Example1Namespace simply inherits from URIIDNamespace
   
+
public class Example1Namespace extends URIIDNamespace {
    // The Java method will process HTTP GET requests
+
 
    @GET
+
public static final String NAME = "ecf.example1.namespace";
    // The Java method will produce content identified by the MIME Media
+
public static final String SCHEME = "ecf.example1";
    // type "text/plain"
+
public static Example1Namespace INSTANCE;
    @Produces("text/plain")
+
    public String getMessage() {
+
/**
        // Return some textual content
+
* The singleton instance of this namespace is created (and registered
        return "Hello World";
+
* as a Namespace service) in the Activator class for this bundle.
    }
+
* The singleton INSTANCE may then be used by both server and client.
 +
*/
 +
public Example1Namespace() {
 +
super(NAME, "Example 1 Namespace");
 +
INSTANCE = this;
 +
}
 +
 
 +
@Override
 +
public String getScheme() {
 +
return SCHEME;
 +
}
 
}
 
}
 
</source>
 
</source>
  
One very useful aspect of the JAX-RS annotations is that multiple implementations may exist, and it is relatively easy to switch from one implementation to another.
+
In the Activator class, the singleton Example1Namespace is created and registered:
 +
 
 +
<source lang="java">
 +
// Create and register the Example1Namespace
 +
context.registerService(org.eclipse.ecf.core.identity.Namespace.class, new Example1Namespace(),  null);
 +
</source>
 +
 
 +
The same Example1Namespace type must be used by both servers and clients, and so it typically makes sense to define the Namespace in a small common bundle, which can be deployed on both servers and clients.  For the source code for this example, see the [https://github.com/ECF/ExampleRSADistributionProviders/tree/master/bundles/org.eclipse.ecf.example1.provider.dist.common bundle here].
 +
 
 +
Note that other type of ID syntax can be easily supported by either inheriting from other Namespace classes (e.g. LongIDNamespace, StringIDNamespace, etc.), or creating one's own Namespace subclass.
 +
 
 +
==Creating the Server Container==
 +
 
 +
 
  
 
==Service-Level Concerns==
 
==Service-Level Concerns==

Revision as of 23:10, 15 March 2016


Introduction

The ECF project provides an implementation of the OSGi R6 Remote Services and Remote Service Admin specifications.

The RSA specification defines two major subsystems: discovery and distribution. Discovery concerns finding remote services exported by other processes on the network. The distribution subsystem is responsible for the actual communication of invoking a remote call: serializing remote method parameters, communicating with the remote service host via some network transport protocol, unmarshalling and invoking the service method with provided parameters, and returning a result to the consumer.

ECF's implementation of RSA defines an abstract API that allows distribution providers to be easily created and used to implement remote invocation. This ECF API is declared in the ECF remote services API, provided by the org.eclipse.ecf.remoteservices bundle. Custom distribution providers simply implement needed part of this API, and then at runtime they will be used to provide the functions of the distribution provider.

This tutorial will show the creation of a simple custom distribution provider. Although there are existing distribution providers, there will continue to be new protocols, new communication styles, and new serialization formats (e.g. JSON, or Protocol Buffers), as well as application or system-specific requirements for security, interoperability, and integration. ECF's pluggable provider approach allows the easy creation of distribution providers that meet these requirements without having to be concerned with implementing all of the OSGi specification requirements.

Remote Service Containers, IDs, and Namespaces=

ECF has the concept of a 'container' (IContainer), which is an object that implements the remote services API, provides a grouping of remote services, and represents a network-accessible endpoint.

Containers have unique transport-specific ID. Some examples of transport-specific container IDs:

https://myhost.com/v1/path/to/service
ecftcp://localhost:3282/server
mqtt://mybroker.com/mytopic
jms://jmsbroker.com:6686/jmstopic
r_osgi://somehost/
hazelcast:///mytopicname

Each ID must be unique within a Namespace.

Implementing a Namespace for a Distribution Provider

The first thing a distribution provider must do is to register a new type of Namespace. ECF provides a number of Namespace classes that can be extended to make this easy. For example, the URIIDNamespace can handle any ID syntax that can be represented as a URI, like all of the above. Here is an example Namespace class that extends the URIIDNamespace:

package org.eclipse.ecf.example1.provider.dist.common;
 
import org.eclipse.ecf.core.identity.URIID.URIIDNamespace;
 
// Example1Namespace simply inherits from URIIDNamespace
public class Example1Namespace extends URIIDNamespace {
 
	public static final String NAME = "ecf.example1.namespace";
	public static final String SCHEME = "ecf.example1";
	public static Example1Namespace INSTANCE;
 
	/**
	 * The singleton instance of this namespace is created (and registered
	 * as a Namespace service) in the Activator class for this bundle.
	 * The singleton INSTANCE may then be used by both server and client.
	 */
	public Example1Namespace() {
		super(NAME, "Example 1 Namespace");
		INSTANCE = this;
	}
 
	@Override
	public String getScheme() {
		return SCHEME;
	}
}

In the Activator class, the singleton Example1Namespace is created and registered:

// Create and register the Example1Namespace
context.registerService(org.eclipse.ecf.core.identity.Namespace.class, new Example1Namespace(),  null);

The same Example1Namespace type must be used by both servers and clients, and so it typically makes sense to define the Namespace in a small common bundle, which can be deployed on both servers and clients. For the source code for this example, see the bundle here.

Note that other type of ID syntax can be easily supported by either inheriting from other Namespace classes (e.g. LongIDNamespace, StringIDNamespace, etc.), or creating one's own Namespace subclass.

Creating the Server Container

Service-Level Concerns

Although frequently of secondary concern to service implementers, service-level concerns are typically of primary importance to service consumers. For example, before I can use any REST-based service, I have to know about (discover) the service and needed meta-data: the service location (i.e. it's URL), what methods exist and arguments are required, and what can be expected in response. Additionally, what version of the service is being accessed, what credentials/security are required, what are the run-time qualities of the remote service?

OSGi Remote Services

In recent releases the OSGi Alliance has defined a specification called Remote Services (chapter 100 in the enterprise spec). Along with the Remote Service Admin specification (chapter 122 in enterprise spec), Remote Services defines ways to express answers to service-level concerns in an implementation-independent way, similar to what JAX-RS does for transport-level concerns. For example, the Remote Service spec allows meta-data for service type, service version, service endpoint/URL identification, security, and quality of service properties to be communicated between service implementer and service consumer without referring to any Remote Services implementation. Further, Remote Services/RSA defines a way to dynamically discover and un-discover remote services, allowing for easy consumer support for networked and frequently unreliable services.

Several implementations of OSGi Remote Services exist, including open source ECF, CXF, Amdatu and commercial implementations.

OSGi RSA has the concept of a distribution system responsible for exporting a local service and making it available for remote access. According to the specification, many distributions systems may be used even for a single service. ECF's implementation is unique because it has an open API for creating/adding new distribution providers. Currently, ECF committers have implemented the following distribution providers

Jax-RS with OSGi Remote Services

Since ECF's RSA implementation implements the OSGi RS and RSA specifications, and Jersey implements the Jax-RS specification, it's possible create and export a remote service that deals with both the transport and service-level concerns in a way completely standardized, and not dependent upon either the OSGi RS/RSA implementation (e.g. ECF), nor on the distribution system provider implementation (Jersey).

For example, here is some example OSGi code for exporting a remote service using the Jax-RS/Jersey provider:

BundleContext bundleContext;
Dictionary<String, String> props = new Hashtable<String,String>();
// osgi rs property to signal to distribution system 
// that this is a remote service
props.put("service.exported.interfaces","*");
// specify the distribution provider with osgi rs property
props.put("service.exported.configs", "ecf.jaxrs.jersey.server");
// as per spec, <provider>.<prop> represents a property intended for use by this provider
props.put("ecf.jaxrs.jersey.server.alias", "/jersey");
// register (and export) HelloImpl as remote service described by Hello interface
bundleContext.registerService(HelloWorldService.class, new HelloWorldResource(), props);

HelloWorldResource implements a HelloWorldService interface which is defined

import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.ws.rs.Path;
 
// The Java class will be hosted at the URI path "/helloworld"
@Path("/helloworld")
public interface HelloWorldService {
 
    // The Java method will process HTTP GET requests
    @GET
    // The Java method will produce content identified by the MIME Media
    // type "text/plain"
    @Produces("text/plain")
    public String getMessage();
}

Note that the HelloWorldService has exactly the same Jax-RS annotations as the HelloWorldResource implementation class.

With the Jax-RS/Jersey distribution provider, the above registerService call will dynamically export the remote service (via Jersey) so that remote clients (Java/OSGi-based or not) can access getMessage by sending a GET to an URL like this:

curl http://localhost:8080/jersey/helloworld/1

Would respond with "Hello World"

The use of Jax-RS annotations to implement an ECF distribution provider, and the use of OSGi Remote Services has several important advantages for the service implementer and the service consumer:

  1. An ability to flexibly handle both transport-level and service-level concerns in a way appropriate to the application
  2. A clean separation between service contract (HelloWorldService interface), and service implementation (HelloWorldResource)
  3. A clean separation between application concerns from transport or service-level concerns
  4. Alternative implementations of Jax-RS and/or OSGi RS/RSA may be substituted at any time without application changes

Using the Remote Service

Since the HelloWorldService is exported via Jax-RS/Jersey provider, it may be accessed by any client (e.g. javascript, java, curl, etc) that can access via http calls. If, however, it is an OSGi client (or a non-OSGi Java Client) ECF Remote Service can automatically construct a proxy for the remote service, and make the proxy available as an OSGi Service on the client. For a description and example of doing this, see Exposing a Jax REST Service as an OSGi Service.

A More Complete Example

A more complex example exists in the Jax-RS/Jersey Provider repo. The remote service host/server is in the com.mycorp.examples.student.remoteservice.host bundle. The remote service consumer/client is in com.mycorp.examples.student.client bundle. Notice that neither of these bundles has references to ECF, Jersey, or even OSGi classes, but rather only to Jax-RS standard annotation types (javax.*) and model classes defined in the com.mycorp.examples.student bundle.

Background and Related Articles

Tutorial: Exposing a Jax REST service as an OSGi Remote Service

Getting Started with ECF's OSGi Remote Services Implementation

OSGi Remote Services and ECF

Asynchronous Proxies for Remote Services

Static File-based Discovery of Remote Service Endpoints

Download ECF Remote Services/RSA Implementation

How to Add Remote Services/RSA to Your Target Platform

Back to the top