OSGI Web Sockets

20 January 2019 Author: Erik Lievaart In this article I will demonstrate the simplest and fastest way to create a web socket under OSGI. I will use apache felix with jetty without any external dependencies. Creating web sockets is relatively simple, but exposing them under OSGI is somewhat confusing. The main problem is that the combination is not well documented.

sample jar

Before I discuss the implementation, you might want to test the solution on your setup first.
You can download a test binary here: Download hello.jar

The following 2 bundles must be deployed to apache felix for the sample jar to work:

org.apache.felix.http.jetty 4.0.6
org.apache.felix.http.servlet-api 1.1.2

implementation

The solution provided here uses only apache felix and jetty. All code can be compiled against the following dependencies:
org.apache.felix.framework 5.6.10
org.apache.felix.http.jetty 4.0.6
org.apache.felix.http.servlet-api 1.1.2

First let's create a simple annotated web socket: EchoWebSocket.java

package com.example;

import java.util.Date;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;

@WebSocket
@SuppressWarnings("unused")
public class EchoWebSocket {

	@OnWebSocketConnect
	public void onConnect(Session session) {
		System.out.println("new socket");
	}

	@OnWebSocketClose
	public void onClose(Session session, int statusCode, String reason) {
		System.out.println("socked closed [" + statusCode + "] " + reason);
	}

	@OnWebSocketMessage
	public void onText(Session session, String message) {
		System.out.println("message received: " + message);
		if (session.isOpen()) {
			session.getRemote().sendString(message + " " + new Date(), null);
		}
	}
}
Nothing special going on here, just a simple class that prints to the console and responds to messages.

Extend Jetty's WebSocketServlet class to create web sockets. The implementation is trivial: MyWebSocketServlet.java

package com.example;

import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

public class MyWebSocketServlet extends WebSocketServlet {
	@Override
	public void configure(WebSocketServletFactory factory) {
		factory.register(EchoWebSocket.class);
	}
}
The last class we need is an osgi BundleActivator. This class is a little trickier.
Registering the WebSocketServlet is easy enough: Activator.java [24:26]
			Hashtable<String, String> properties = new Hashtable<>();
			properties.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/socket");
			service = context.registerService(Servlet.class, new MyWebSocketServlet(), properties);
but you will run into class loading issues:
Caused by: java.lang.ClassNotFoundException: org.eclipse.jetty.websocket.server.WebSocketServerFactory
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at org.eclipse.jetty.websocket.servlet.WebSocketServletFactory$Loader.load(WebSocketServletFactory.java:47)
	... 35 more
You can get around this problem by getting the class loader of the jetty bundle and setting it as the context class loader before registering the Servlet. This ensures the relevant classes are available when the servlet is initialized. It is not a clean solution, but it works.

You can look up the jetty bundle using it's symbolic name: Activator.java [37:44]

	private Bundle getJettyBundle(BundleContext context) {
		for (Bundle bundle : context.getBundles()) {
			if (bundle.getSymbolicName().equals("org.apache.felix.http.jetty")) {
				return bundle;
			}
		}
		throw new RuntimeException("Jetty not found");
	}
Next, we adapt the Bundle to the BundleWiring interface to get the ClassLoader: Activator.java [21:21]
		ClassLoader jettyClassLoader = getJettyBundle(context).adapt(BundleWiring.class).getClassLoader();
Finally, we assign it as the context class loader: Activator.java [22:22]
		Thread.currentThread().setContextClassLoader(jettyClassLoader);
Here is the full class: Activator.java
package com.example;

import java.util.Hashtable;

import javax.servlet.Servlet;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;

public class Activator implements BundleActivator {

	private ServiceRegistration<Servlet> service;

	@Override
	public void start(BundleContext context) throws Exception {
		ClassLoader original = Thread.currentThread().getContextClassLoader();
		ClassLoader jettyClassLoader = getJettyBundle(context).adapt(BundleWiring.class).getClassLoader();
		Thread.currentThread().setContextClassLoader(jettyClassLoader);
		try {
			Hashtable<String, String> properties = new Hashtable<>();
			properties.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/socket");
			service = context.registerService(Servlet.class, new MyWebSocketServlet(), properties);
		} finally {
			Thread.currentThread().setContextClassLoader(original);
		}
	}

	@Override
	public void stop(BundleContext context) throws Exception {
		service.unregister();
	}

	private Bundle getJettyBundle(BundleContext context) {
		for (Bundle bundle : context.getBundles()) {
			if (bundle.getSymbolicName().equals("org.apache.felix.http.jetty")) {
				return bundle;
			}
		}
		throw new RuntimeException("Jetty not found");
	}
}
Compile the classes and create a jar with a valid osgi manifest:
Manifest-Version: 1.0
Bundle-Name: example-hello
Bundle-SymbolicName: com.example.hello
Bundle-Version: 1.0.0
Bundle-Activator: com.eriklievaart.tiqqer.hello.Activator
Import-Package: org.osgi.framework,org.osgi.framework.wiring,org.osgi.
 service.http.whiteboard,javax.servlet,org.eclipse.jetty.websocket.api
 ,org.eclipse.jetty.websocket.api.annotations,org.eclipse.jetty.websoc
 ket.servlet
Once you have deployed the bundle, you can test it with the web socket tester from my other article: Web Socket Tester On default settings it is available on
ws://localhost:8080/socket
The port of the OSGI HTTP server can be changed using the following config property:
org.osgi.service.http.port=[port]