Die Http Whiteboard Specification

Webkomponenten mit OSGi – einfach und elegant

Carsten Ziegeler

© istockphoto / xiaoke ma

Nahezu jede Webanwendung in Java basiert auf dem Servlet-API. Seit geraumer Zeit wird ein Update der OSGi Http Service Specification erwartet, die es endlich erlaubt, heutzutage gängige Features des Servle-API zu nutzen. Mit der neuen Http Whiteboard Specification liegt das Update nun vor. Dieser Artikel erläutert die Grundlagen zur Entwicklung von Webkomponenten mit OSGi auf Basis dieser Neuerungen.

Die Http-Service-Spezifikation aus dem OSGi Compendium ist mittlerweile in die Jahre gekommen. Sie erlaubt es, auf einfache Weise Servlets und Ressourcen nach dem Servlet-API 2.1 aus dem Jahre 1998 zu registrieren. Dem gegenüber bietet die aktuelle Version 3.1 des Servlet-API eine Fülle an nützlichen Funktionen und weiteren Webelementen wie beispielsweise Filter und diverse Listener. Möchte man diese mit OSGi nutzen, so war man in der Vergangenheit auf nicht standardisierte Erweiterungen des Http-Service angewiesen.

Ende des zweiten Quartals 2015 wird die neue Version R6 der OSGi-Compendium-Spezifikation erwartet, die unter anderem die neue Http-Whiteboard-Spezifikation enthält. Diese erlaubt die Verwendung der bekannten Webelemente aus der aktuellen Servlet-Spezifikation auf der bei OSGi üblichen Art und Weise: Eigene Webkomponenten können bequem per Whiteboard-Pattern in der Serviceregistrierung eingetragen werden. Im Gegensatz zum Http-Service erfordert die neue Spezifikation nicht die Verwendung eines zentralen Service. Die aus üblichen Webanwendungen bekannten Verfahren, wie Deployment Descriptor oder Annotations, finden in einer reinen OSGi-Anwendung keine Verwendung und werden daher von der Spezifikation nicht unterstützt.

Neben den wichtigsten Komponenten, den Servlets, werden Filter, Ressourcen und Listener in der OSGi-Service-Registrierung angemeldet. Die Implementierung der Http-Whiteboard-Spezifikation greift die vorhandenen Services von dort automatisch ab und macht sie entsprechend für Webaufrufe verfügbar. Über einen Runtime-Service kann der aktuelle Zustand abgefragt werden: die registrierten und verwendeten Webkomponenten, die aktuell nicht benutzten und die Services, bei denen Fehler auftraten. Doch bevor wir zu diesem Teil kommen, sollten wir ein paar Beispiele anschauen. Die folgenden Codefragmente nutzen die neue Declarative Services Specification (siehe: Ziegeler, Carsten: „OSGi-Komponenten – einfach gemacht“, in Eclipse Magazin 3.2015). Sie lassen sich einfach auf andere Entwicklungsansätze für OSGi-Komponenten übertragen.

@Component(service = Servlet.class , 
           property= "osgi.http.whiteboard.servlet.pattern=/hello",
           scope=ServiceScope.PROTOTYPE)
public class HelloWorldServlet extends javax.servlet.http.HttpServlet {
    
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
            resp.getWriter().println("Hello World");
        }
    }

In Listing 1 wird ein eigenes Servlet unter dem Pfad /hello registriert. Das entsprechende Pattern wird über das vordefinierte Service-Property osgi.http.whiteboard.servlet.pattern gesetzt. Für den Wert gelten die gleichen Regeln wie im Servlet-API zur Registrierung für Servlets. Für einen entsprechenden Request wird der Text „Hello World“ zurückgegeben. Der Http-Whiteboard-Service holt den Servlet-Service aus der Serviceregistrierung und ruft die init()-Methode auf. Von nun an kann das Servlet über Requests aufgerufen werden. Wird das Servlet nicht mehr verwendet, so wird destroy() aufgerufen. Dadurch ist der gleiche Lebenszyklus wie in einer normalen Webanwendung gewährleistet.

Webkomponenten sollten immer mit dem Scope Prototype registriert werden. Der Scope Prototype ist eine neue Eigenschaft vom OSGi Core R6. Hierbei wird für jede Verwendung dieses Service eine neue Instanz erstellt. Durch das Whiteboard-Pattern kann es vorkommen, dass der Servlet-Service durch eine Http-Whiteboard-Implementierung benutzt wird. Bei Änderungen kommt er zeitweise nicht zur Anwendung, kann jedoch später wiederverwendet werden. Der einfachste Fall ist ein Update der Http-Whiteboard-Implementierung zur Laufzeit. Wenn der Servlet-Service nicht als Prototype registriert ist, würde dieses bedeuten, dass init() und destroy() mehrfach auf der gleichen Instanz aufgerufen werden. Das ist allerdings laut Servlet-Spezifikation nicht erlaubt. Aus diesem Grund sollten Servlets und Filter immer als Prototype registriert werden.

Hello World und mehr

Die Registrierung von Filtern ist ähnlich einfach wie die Registrierung von Servlets. Ein Filter kann auf verschiedene Arten eingebunden werden: per Pattern, für ein bestimmtes Servlet oder über einen regulären Ausdruck. In Listing 2 wird ein einfacher Filter mit dem Pattern /* registriert und damit für jeden Request aufgerufen. Da es für einen Request mehrere Filter geben kann, werden diese in einer fest definierten Reihenfolge abgearbeitet: Der Service mit dem höchsten Service-Ranking wird zuerst durchlaufen. Sollten zwei Services das gleiche Ranking besitzen, wird derjenige mit der niedrigeren Service-ID zuerst aufgerufen. Dies ermöglicht eine stabile Reihenfolge für Filter, unabhängig von der Abfolge, in der die Services registriert werden.

@Component(property= "osgi.http.whiteboard.filter.pattern=/*",
           scope=ServiceScope.PROTOTYPE)
public class MyFilter implements javax.servlet.Filter {
        
    public void init(javax.servlet.FilterConfig cfg) {
    }
    public void destroy() {
    }

    public void doFilter(ServletRequest req, ServletResponse resp,
                         FilterChain chain) {
        // do something before the servlet
        chain.doFilter(req, resp);
        // do something after the servlet
    }
    }

Über die Dispatcher-Service-Property kann der Filter ausschließlich an bestimmte Request-Arten gebunden werden, beispielsweise nur an Includes, Forwards, asynchrone Requests oder aber an die Fehlerbehandlung. Für eine vernünftige Antwort im Fehlerfall können auch Servlets registriert werden, die entweder bei einem bestimmten Fehlerstatuscode oder aber bei einer bestimmten Exception aufgerufen werden. Dadurch lässt sich die Fehlerbehandlung ebenfalls bequem über OSGi managen.

Servlets und Filter eignen sich hervorragend für dynamische Inhalte. Webanwendungen weisen allerdings auch immer einen statischen Anteil auf, zum Beispiel Bilder, CSS- oder JavaScript-Dateien. Diese können über Ressourcen ausgeliefert werden. Dabei sind die Dateien Bestandteil eines Bundles und werden über eine entsprechende Serviceregistrierung zur Verfügung gestellt.

@Component(service = MyResourceService.class , 
           property= {"osgi.http.whiteboard.resource.pattern=/static/*",
                      "osgi.http.whiteboard.resource.prefix=/www"})
public class MyResourceService{
    // nothing to implement
    }

In Listing 3 erfolgt eine Ressourcenregistrierung. Zu beachten ist hier, dass die Klasse lediglich zur Anmeldung in der Serviceregistrierung benötigt wird. Ansonsten hat sie keine weitere Funktion und daher kann direkt die Klasse als Service zur Registrierung verwendet werden. Wichtig sind allerdings die beiden Properties. Das Property osgi.http.whiteboard.resource.pattern definiert das Pattern, über das die Ressourcen erreichbar sind, und osgi.http.whiteboard.resource.prefix gibt den Suchpfad für die Dateien innerhalb des Bundles an. So würde für das Beispiel ein Request an /static/script.js eine Ressource unter /www/script.js aus dem Bundle zurückliefern.

Eigene Listener können ebenso bequem als Service registriert werden. Die Http-Whiteboard-Spezifikation unterstützt die folgenden Listener durch Services:

  • ServletContextListener: Benachrichtigung, wenn ein Kontext aktiv oder inaktiv wird
  • ServletContextAttributeListener: Aufruf bei Änderungen von Servlet Kontext Attributen
  • ServletRequestListener: Benachrichtigung über jeden Request
  • ServletRequestAttributeListener: Nachricht über Änderungen von Servlet-Request-Attributen
  • HttpSessionListener: Aufruf bei Erstellen und Löschen einer Session
  • HttpSessionAttributeListener: Benachrichtigung bei Änderungen von Attributen in der Session
  • HttpSessionIdListener: Aufruf bei Änderungen einer Session-ID

Die Listener funktionieren und werden wie in der Servlet-Spezifikation definiert aufgerufen. Allerdings gibt es ein paar Punkte zu beachten: Die Listener erhalten nur Nachricht über Änderungen, die nach ihrer Registrierung aufgetreten sind. Damit ein Listener von der Http-Whiteboard-Implementierung aus der Serviceregistrierung aufgegriffen wird, muss er die Property osgi.http.whiteboard.listener mit dem Wert true setzen. Andernfalls wird er ignoriert.

Listing 4 zeigt einen ServletRequestListener, der zu Anfang und Ende eines jeden Requests die Adresse des Clients ausgibt. Da Listener keine init– oder destroy-Methode haben, können sie als Singleton registriert werden.

@Component(property = "osgi.http.whiteboard.listener=true")
public class MyServletRequestListener implements ServletRequestListener {
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("Request initialized for " 
            + sre.getServletRequest().getRemoteAddr());
    }

    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("Request destroyed for " 
            + sre.getServletRequest().getRemoteAddr());
    }
    }

Die Http-Whiteboard-Spezifikation definiert für jeden Webkomponententyp eigene Service-Properties. Dadurch kann eine registrierte Komponente auch mehrere Services, zum Beispiel Servlet und Filter, gleichzeitig implementieren.

Meine App, deine App

Servlets, Filter, Ressourcen und Listener bilden den Grundstein für eine eigene Webanwendung. Während innerhalb eines Application Servers mehrere Webanwendung parallel in unterschiedlichen Kontexten installiert werden können, erlaubt die Http-Whiteboard-Spezifikation eine ähnliche Trennung durch unterschiedliche Servlet-Kontexte. Ohne weitere Angaben bei der Serviceregistrierung wird jeder Service mit dem Default-Kontext assoziiert. Wird mehr als ein Servlet innerhalb eines Kontexts für das gleiche Pattern registriert, so wird nur das Servlet mit dem höheren Service-Ranking verwendet.

Obwohl in diesem Fall zwei Services in der Registrierung vorhanden sind, wird nur einer genommen. Eine solche Überlappung ist als solche bei großen Anwendungen schwer zu erkennen. Zum Glück definiert die Http-Whiteboard-Spezifikation den Runtime-Service. Dieser stellt den aktuellen Zustand durch entsprechende Statusobjekte zur Verfügung. Darüber kann leicht festgestellt werden, ob zum Beispiel eine erfolgreiche Servlet-Registrierung durch eine andere überlagert wird. Genauso lässt sich feststellen, welche Registrierungen fehlerhaft sind und welche erfolgreichen Registrierungen an einen Servlet-Kontext gebunden sind.

Eine eigene Webanwendung kann von anderen isoliert werden, indem alle zugehörigen Webkomponenten an einen eigenen Servlet-Kontext gebunden werden. Dieses wird durch die Service-Property osgi.http.whiteboard.context.select erreicht. Die häufigste Verwendung wird sicherlich das Binden an einen Kontext mit einem bestimmten Namen sein. Der Wert dieser Property ist ein LDAP-Filter. Möchte man ein Servlet an den Kontext myapp binden, so ist der Wert der Property (osgi.http.whiteboard.context.name=myapp). Allerdings besteht ein solcher Kontext nur dann, wenn auch eine entsprechende Serviceregistrierung für den Kontext selbst existiert. Listing 5 zeigt eine solche Registrierung zusammen mit einem Servlet, das an den Kontext gebunden wird.

@Component(service = ServletContextHelper.class,
           property = {"osgi.http.whiteboard.context.name=myapp",
                       "osgi.http.whiteboard.context.prefix=/game"},
           scope=ServiceScope.BUNDLE)
public class MyAppContext extends ServletContextHelper {
    // usually no need to implement methods 
    }
@Component(service = Servlet.class , 
           property= {"osgi.http.whiteboard.servlet.pattern=/hello",
                      "osgi.http.whiteboard.context.select=" +
                        "(osgi.http.whiteboard.context.name=myapp)"},
           scope=ServiceScope.PROTOTYPE)
public class HelloWorldServlet extends javax.servlet.http.HttpServlet {
}

Der Kontext wird als ServletContextHelper-Service registriert. Die Klasse ServletContextHelper ist eine abstrakte Klasse, die für alle Methoden eine vernünftige Default-Implementierung liefert. Der Kontext sollte Bundle als Service-Scope angeben, damit für jedes Bundle, das diesen Kontext verwendet, eine eigene Instanz erstellt wird. Dies ist wichtig, damit Ressourcen aus dem richtigen Bundle für Ressourcenregistrierungen aufgelöst werden.

Neben dem Namen wird ein Kontext immer mit einem Präfix registriert. Es erlaubt eine Trennung des URL-Raums für unterschiedliche Anwendungen. In dem Beispiel aus Listing 5 wird der Kontext mit dem Präfix /game registriert und das Servlet mit dem Pattern /hello. Da das Servlet an den Kontext gebunden ist, ist es über den Pfad /game/hello erreichbar. Sobald eine eigene Webanwendung per OSGi Service definiert wird, sollten sämtliche Webkomponenten an einen eigenen Kontext gebunden werden, damit die Webanwendung ohne Probleme mit anderen Anwendungen koexistieren kann. Über das Präfix-Property kann eine Webanwendung bequem und einfach an einen anderen Pfad verlegt werden. Sämtliche Filter und Listener kommen nur zum Einsatz für Requests und Aktionen, die sich auf diesen Kontext beziehen. Und daraus ergibt sich ein Unterschied für ServletContextListener zwischen einer normalen Webanwendung und einer OSGi-Anwendung: In OSGi erhalten diese Listener immer Benachrichtigungen über den Zustand des Kontexts, unabhängig davon, wann sie registriert bzw. aus der Registrierung entfernt werden. Da aber auch Kontexte dynamisch registriert werden, können mehrere solcher Nachrichten für einen Kontext auftreten.

Meilensteine

Durch den konsequenten Einsatz des Whiteboard-Patterns in der neuen Http-Whiteboard-Spezifikation wird die Entwicklung von Webanwendungen mit OSGi einfach und elegant. Anwendungen können endlich Möglichkeiten der aktuellen Spezifikation des Servlet-API nutzen, und OSGi-Anwendungen stehen normalen Webanwendungen in nichts mehr nach. Der Runtime-Service ist sehr nützlich, da sich dadurch leicht Fehler und Probleme finden lassen. Die in diesem Artikel vorgestellten Neuerungen finden sich aktuell in dem RFC 189, der für das kommende R6-Release des OSGi Compendium genutzt wird.

Mit der Veröffentlichung Ende des 2. Quartals 2015 stehen entsprechende Open-Source-Implementierungen bereit. Schon heute ist die Http-Implementierung aus dem Apache-Felix-Projekt die Referenzimplementierung und setzt die neue Spezifikation weitestgehend um. Ebenso finden sich im Apache-Felix-Projekt Implementierungen von OSGi Core R6, Declarative Services und weiteren Spezifikationen. Somit steht einer effizienten Entwicklung von OSGi-Webkomponenten nichts mehr im Wege. Happy Coding!

Geschrieben von
Carsten Ziegeler
Carsten Ziegeler
Carsten Ziegeler leitet das Infrastrukturteam bei Adobe Research, ist im Board der OSGi Alliance und arbeitet in den OSGi-Expertengruppen mit. Als Mitglied der Apache Software Foundation arbeitet er unter anderem an den OSGi-Projekten Apache Felix und Apache Sling.
Kommentare

1
Hinterlasse einen Kommentar

avatar
4000
1 Kommentar Themen
0 Themen Antworten
0 Follower
 
Kommentar, auf das am meisten reagiert wurde
Beliebtestes Kommentar Thema
1 Kommentatoren
Mike Letzte Kommentartoren
  Subscribe  
Benachrichtige mich zu:
Mike
Gast
Mike

Hallo Carsten,

ich suche nach einer Möglichkeit Filter übergreifend über alle WebContainer zu definieren. Anwendungsfall ist z.B. ein Multi-Domain-System, bei der der Filter die Domain analysiert und einen Context für den Request bereit stellt oder ein zentrales Berechtigungssystem über alle WebContainer.

Bis jetzt habe ich mir geholfen indem ich den Jetty gepatcht habe und properitär ein Filtersystem eingebaut habe. Ausgiebige Tests haben leider gezeigt, dass die Container in der Servlet-Implementation strikt getrennt sind.

Ist dieser Fall nun durch HTTP Whiteboard standardisiert umsetzbar, leider habe ich das nicht aus dem Artikel herauslesen können.

Freue mich auf Antwort,

Mit Gruß,
Mike