Effiziente Entwicklung von REST-Anwendungen mit dem Apache-Sling-Framework

REST in Peace, Sling!

Olaf Otto, Daniel Takai

©Shutterstock/Liudmyla Marykon

Mit seiner radikalen Implementierung der REST-Architektur war das Apache-Sling-Framework seiner Zeit zu weit voraus und lange Zeit den Benutzern des Content-Management-Systems Adobe AEM vorbehalten. Dabei komponiert das Open-Source-Framework auf einzigartige Weise Architekturstandards zu einer skalierbaren Lösung. Zeit, ihm mehr Beachtung zu schenken.

Dieser Artikel ordnet Sling in die bestehende Webframeworklandschaft ein und gibt einen Überblick über seine Architektur. Anschließend werden Anwendungs- und Codebeispiele gezeigt, die die besonderen Eigenschaften von Sling illustrieren. Der Artikel schließt mit der Betrachtung der aktuellen Entwicklung und der Zukunft des Frameworks ab.

Web was?

Wer versucht, die bestehende Landschaft von Webframeworks zu überblicken, wird von der Vielzahl an Lösungen und Begriffen erschlagen. Wikipedias Liste der wichtigsten Webframeworks kennt unzählige, in mehr als dreizehn verschiedenen Programmiersprachen geschriebene Varianten. Was sind ihre Gemeinsamkeiten?

Die Säulen des Webs

Wer vom Web spricht, spricht von HTTP. Entsprechend stellen alle Frameworks zunächst einen HTTP-Server zur Verfügung oder benötigen einen solchen. Wer von HTTP spricht, spricht natürlich über dessen Methoden und über URIs, also über die Verben und Subjekte des Webs. Diese definieren die zweite Charakteristik der Frameworks: Sie stellen einen Weg zur Verfügung, URIs im Zusammenhang mit HTTP-Methoden und -Headern Ressourcen zuzuordnen, was als Routing bezeichnet wird.

Bleibt noch der Konversationsinhalt: Im Web werden Informationen primär in JSON, XML und HTML repräsentiert. Folgerichtig unterstützen Webframeworks meist alle diese Formate als Repräsentation ihrer Ressourcen. Client/Server, HTTP, HTML, URIs, Ressourcen – es ist kein Zufall, dass genau diese Elemente die Eckpfeiler des zentralen Architekturstils des Webs bilden: REST.

Sind alle Webframeworks RESTful?

Im Prinzip ja. Wie könnte es auch anders sein – im Grunde genommen ist das Web selbst eine REST-Implementierung. Zudem ist REST-Stil weniger restriktiv als weithin angenommen. Das verwenden „exotischer“ URIs, der Gebrauch von Query-Parametern als Befehle – ja sogar Sessions müssen (zumindest in der Theorie) kein Verstoß gegen diesen Architekturstil sein. Zwar verbietet REST explizit serverseitigen Konversationszustand, würde man die Inhalte der Session jedoch als Ressource auffassen und den Session-Zustand als Ergebnis einer Serie von HTTP-Operationen der Form <Methode> /resource/<id>, so wären Sessions prinzipiell REST-konform. Auch die Verwendung von Cookies ist mit REST vereinbar – schließlich sind Cookies ein rein clientseitiger Zustand, was zulässig ist. Der Verstoß gegen den REST-Stil resultiert erst aus den fragwürdigen Details von Implementierungen, die Session-Daten zu einem nicht adressierbaren Nebenprodukt unserer Webkonversation machen. Die Java-Servlet-Spezifikation für Sessions ist hierfür ein prominentes Beispiel.

Entscheidender für eine REST-Anwendung ist die Einhaltung der semantischen Constraints. Per URI adressierte Elemente werden als Ressource aufgefasst, die vom Server an den Client übertragen, dort gegebenenfalls modifiziert und dann wieder zurück übertragen wird. Dazu sollten die von HTTP zur Verfügung gestellten Methoden nach Spezifikation verwendet werden.

Gute Frameworks, schlechte Frameworks

Bei der Auswahl eines Frameworks für eine REST-Anwendung lautet somit die zentrale Fragestellung, wie geradlinig die Begrifflichkeiten von REST übersetzt werden bzw. welcher Konversionsaufwand den Anwendern des Frameworks auferlegt wird. Wie viel Code muss man schreiben, um von einer HTTP-Anfrage zu einer Ressource und von dieser zu seiner Repräsentation zu gelangen?

Native REST mit Apache Sling

Für Apache Sling, das seit 2009 bei der Apache Software Foundation entwickelt wird, ist diese Frage rasch beantwortet: Das Framework erlaubt nicht eine REST-Implementierung, es ist eine REST-Implementierung. Alle in Sling vorhandenen Elemente wie Inhalte, Servlets und Skripte sind Ressourcen, besitzen einen URI und sind per HTTP adressierbar, inklusive einer Standardrepräsentation in JSON und XML. Für Sling lautet somit die Antwort auf die Frage nach dem Konversionsaufwand: Es gibt keinen. Doch der Reihe nach.

Die Architektur von Sling „in a nutshell“

Sling besteht aus einem Launchpad, das einen OSGi-Server (Apache Felix) startet. In diesem laufen alle Komponenten des Frameworks zusammen mit den auf dem Framework basierenden Applikationen als OSGi Bundles (Abb. 1). Ein Java Content Repository (JCR) stellt die Persistenzschicht des Frameworks zur Verfügung, während mit Eclipse Jetty ein moderner Servlet-Container Verwendung findet.

Abb. 1: Horizontale Sling-Architektur

Abb. 1: Horizontale Sling-Architektur

Man kann die Architektur von Sling als horizontal bezeichnen – im Gegensatz zu vertikalen Architekturen, die den Zugriff auf ihre Schichten oft nur indirekt ermöglichen, ist Sling eine Komposition gleichberechtigter Akteure. Dank OSGi kommunizieren diese über Services, für die umfangreiche APIs zur Verfügung stehen. Auch Kernfunktionalitäten des Frameworks können so auf saubere Weise angepasst werden.

Ebenfalls sticht hervor, dass Sling mit dem JCR eine eigene Persistenzschicht beinhaltet. Diese ist bei der Auflösung von URIs zu Ressourcen zentral, da das JCR als hierarchischer Datenspeicher die konzeptuelle Konsistenz zwischen den hierarchischen URIs und der Datenrepräsentation im Backend gewährleistet.

Everything is Content

Dem JCR fällt eine besondere Rolle zu: In ihm werden alle Inhalte der Webapplikation – wie Texte, Bilder und Skripte – verwaltet, was diese nicht nur per HTTP adressierbar, sondern auch durchsuchbar und versionierbar macht. Mittels Observation bzw. Event Listening bietet das Repository zudem die Möglichkeit, auf Inhaltsänderungen zu reagieren.

Das JCR ist einerseits mit einem Dateisystem vergleichbar, d. h. es erlaubt die hierarchische Ablage unstrukturierter Daten. Andererseits stellt es ein mächtiges Typsystem zur Verfügung, das auch Mixins und Vererbung beherrscht. Hiermit lassen sich auch strukturierte Daten verwalten.

Die Funktionsweise von Sling: Routing

Wie also löst Sling eine HTTP-Anfrage auf einen Inhalt auf? Jeder HTTP-Request ist mit einem ResourceResolver assoziiert, der mit den Rechten des anfragenden Benutzers (per default „anonymous“) ausgestattet ist. Der Resolver findet den ResourceProvider-Service, der eine Ressource für den Pfadteil des URI liefert. Inhalte aus dem JCR stellt der JcrResourceProvider zur Verfügung, andere ResourceProvider können ergänzt werden.

Rendering: von der Ressource zum View

Kann für den Pfad eine Ressource aufgelöst werden, so folgt die Auflösung der View (Abb. 2). Während dies bei Servlets der Aufruf der entsprechenden Servlet-Methode ist (beispielsweise doGet oder doPost), wird bei JCR-Inhalten eine Property namens sling:resourceType zur Auflösung der View verwendet. Diese Property beinhaltet einen relativen Pfad, der – konform zur REST-Philosophie von Sling – auf eine Ressource aufgelöst wird. Sling verwendet diese Ressource, um eine View zu bestimmen. Views sind Skripte im Sinne von javax.scripting, wobei zwischen Servlets und Skripten kein Unterschied besteht. Das Skript wird mit der aufgelösten Ressource (und zusätzlichen kontextuellen Informationen) ausgeführt und liefert deren gerenderte Repräsentation. Sling unterstützt dabei zahlreiche Skriptsprachen. Seit Kurzem steht mit Sightly eine auf HTML5 fokussierte Sprache mit standardmäßigem XSS-Schutz zur Verfügung.

Der sling:resourceType wird durch den ResourceResolver aufgelöst, indem diesem relativen Pfad Präfixe aus einer konfigurierbaren Liste (dem Search Path) vorangestellt werden. Die aufgelöste Ressource repräsentiert den Resource Type. Ihre Kinder sind die zur Verfügung stehenden View-Skripte. Für die Auswahl eines Skripts werden die Pfad-Extension (beispielsweise .html), die HTTP-Methode und/oder Selektoren, d. h. vor der Extension platzierte und durch Punkt separierte Pfadbestandteile, verwendet. Dies ermöglicht es Clients, eine gewünschte Repräsentation explizit zu adressieren. Bei der Auflösung des Skripts wird außerdem die Sling Resource Type Hierarchy verwendet. Durch Angabe eines sling:resourceSuperType können Skripte von Supertypen „ererbt“ oder überschrieben werden. Für JSON und XML stellt Sling Standard-Views bereit, die bei Bedarf überschrieben werden können.

Abb. 2: Sling Resource Resolution

Abb. 2: Sling Resource Resolution

Domänenspezifische Applikationslogik

Die Verarbeitung von Requests kann durch die als OSGi Bundles zur Verfügung gestellten Applikationen angepasst werden. Neben den bekannten javax.servlet-Akteuren wie Servlets und Filtern kann ein Bundle eigene Ressourcen oder JCR-Content bereitstellen, Resource-Provider in die Resource Resolution einbringen (beispielsweise um Datenbankinhalte als Ressourcen zu exponieren) oder aufgelöste Ressourcen mit Resource Decorators erweitern.

Domänenspezifische Logik lässt sich beispielsweise in Form von OSGi Services implementieren,die den Skripten oder anderen Services zur Verfügung gestellt werden. Abhängigkeiten zu anderen Services können mittels Annotationen injiziert werden. Dank der ebenfalls über Annotationen generierten Service-Component-Runtime-(SCR-)Metadaten lassen sich Services zur Laufzeit über eine Webkonsole konfigurieren, beispielsweise um Mailservereinstellungen ohne Deployment zu ändern.

Des Weiteren steht mit Adapter Factories eine mächtige Abstraktion zur Verfügung, mit der Factories für domänenspezifische Modelle zur Verfügung gestellt werden können. Modellinstanzen können dann mit dem Aufruf <adaptable>adaptTo(TargetType.class) ohne Kenntnis der Factory bezogen werden.

Die Bestandteile einer Sling-basierten Applikation lassen sich flexibel schneiden: Für einfache Anwendungsfälle reicht die Ablage von Inhalten und Skripten im JCR. Steigt die Komplexität, kann Backend-Logik in OSGi Bundles zur Verfügung gestellt werden. Die Verwendung von OSGi ermöglicht es, umfangreiche Applikationen in isolierte Module aufzuteilen und diese über Services kooperieren zu lassen. Diese Struktur passt perfekt mit modularen Build-Technologien wie Maven zusammen, da die Modulaufteilung zur Compile-Zeit der Modulaufteilung zur Laufzeit entsprechen kann. Dabei zeigt das Sling-Framework eine geringe Dominanz: Bis auf die Tatsache, dass Applikationscode in Form von OSGi Bundles vorliegen sollte, gibt es nur wenige Regeln. Dies hat zur Folge, dass auf Sling basierende Anwendungen durch ihre Semantik und nicht durch das Framework dominiert werden. Gleiches gilt für den Content: Da das JCR einen beliebigen Grad an Strukturierung erlaubt, kann der Content gemäß den individuellen Anforderungen gestaltet werden – Content first, structure later.

Beispiel: Ein Webprojekt mit Sling

Für diesen Artikel haben wir ein Beispielprojekt auf GitHub zur Verfügung gestellt, das einige der angesprochenen Features demonstriert. Das Projekt besteht aus einem OSGi Bundle, das Content, Skripte und Applikationslogik enthält, um eine Website auf Basis eines bestehenden Sets von HTML-Templates zu implementieren.

Anlegen von JCR-Ressourcen

JCR-Content kann direkt aus Bundles heraus erzeugt werden. Das Beispielprojekt importiert Content unter anderem aus der in SLING-INF/content abgelegten JSON-Datei javamagazin.json. In JSON werden der Primary Type (Node Type), die im JCR anzulegenden Inhalte und deren Properties definiert (Listing 1).

{
 "jcr:primaryType": "nt:unstructured", // JCR node type
 "sling:resourceType": "javamagazin/components/page", // Sling Type (Script resolution) 
 "title": "Hi. This is Directive." // Beliebige Properties
}

Für den Import von Content aus einem Bundle wird der Manifest-Header Sling-Initial-Content in das Bundle-Manifest eingetragen. Im Beispielprojekt geschieht dies durch die Konfiguration des Maven-Bundle-Plug-ins in der POM (Listing 2).

      SLING-INF/content/text;overwrite:=true,
      SLING-INF/content/binary;overwrite:=false,
      SLING-INF/content/views;overwrite:=true
    
    
      /javamagazin/static/;path:=/SLING-INF/static-resources/
    

Aus SLING-INF/content/binary werden zusätzlich binäre Inhalte in die durch die JSON-Datei erstellten Inhalte eingebracht. Die Aufteilung in Views und Content dient lediglich der besseren Verständlichkeit.

Statische Dateien könnten auch direkt aus einem Bundle ausgeliefert werden. Hierzu werden die Dateien im Bundle abgelegt und unter dem im Manifest-Header Sling-Bundle-Resources definierten Pfad zur Verfügung gestellt. Im Hintergrund erstellt Sling automatisch einen Resource-Provider, der die Ressourcen unter dem angegebenen Pfad bereitstellt. Im Beispielprojekt werden statische Dateien des Templatesets unter /javamagazin/static/ ausgeliefert (Listing 2).

Bereitstellung von HTML-Views

Der Content des Beispielprojekts verwendet Resource Types der Form javamagazin/components/<xyz>, die durch die Script Resolution zu /apps/javamagazin/components/<xyz> aufgelöst werden. Das Bundle beinhaltet die passenden Skripte, wobei JSP als Skriptsprache verwendet wurde. Nach seiner Installation stehen die in Abbildung 3 gezeigten Ressourcen zur Verfügung.

Abb. 3: Ressourcen des Beispiels

Abb. 3: Ressourcen des Beispiels

Komposition von HTML-Views

Um auf die Properties der Ressource zuzugreifen, stehen diese als Map unter dem Namen properties zur Verfügung. Hierzu wird die Sling Tag Library verwendet, die durch den Aufruf von .adaptTo(ValueMap.class) auf der aktuellen Ressource indirekt eine der Standard-Adapter-Factories von Sling verwendet:

 
<sling:adaptTo adaptable="${resource}" adaptTo="org.apache.sling.api.resource.ValueMap" var="properties"/>

Die Webseite wird ihrer Semantik gemäß in einzelne Ressourcen (Header, Footer, Navigation …) zerlegt. Die HTML-View der Seitenressource ist dann eine Komposition der HTML-Views dieser Ressourcen. Die Komposition erfolgt durch Inklusion der Bestandteile im page.jsp-Skript (schematische Darstellung in Listing 3):

<html>
<body>
<sling:include path="header" resourceType="javamagazin/components/header" />
...
<sling:include path="footer" resourceType="javamagazin/components/footer" />
</body>
</html>

Das path-Attribut des Include-Tags zeigt auf eine Ressource, die mit dem angegebenen Resource Type gerendert wird. Der Pfad wird relativ zur aktuellen Ressource interpretiert, kann aber auch absolut sein. Die Angabe des Resource Types ist nur dann nötig, wenn die inkludierte Ressource (noch) nicht existiert oder der Resource Type für das Rendering überschrieben werden soll.

Da Ressourcen in Sling über einen eigenen URI verfügen, könnten die im page-Skript inkludierten Bestandteile auch per AJAX geladen werden – der Header bspw. via /javamagazin/header.html oder als JSON via /javamagazin/header.json. Die Beispielanwendung nutzt diese Option, um mittels eines Servlet-Filters (EditContentFilter) und eines Skripts (forms.js) Formulare zum Bearbeiten der Seite anzuzeigen, die mit den Daten der jeweiligen Ressource befüllt sind.

… und etwas Applikationslogik

Das Kontaktformular des Projekts soll beim Absenden Content im JCR Repository erzeugen. Hierzu wird es per POST an einen Ressourcenpfad gesendet. Wird Content im JCR verändert, erzeugt Sling Events, auf die beliebig viele Event Handler reagieren können. Der ContactRequestJobGenerator des Beispielprojekts reagiert auf das Anlegen neuer Kontaktanfragen und erzeugt für sie einen Sling-Job (Listing 4).

@Component(immediate = true)
@Service
@Properties({
  @Property(name = EVENT_TOPIC, value = TOPIC_RESOURCE_ADDED),
  @Property(name = EVENT_FILTER, value = "(&" +
    "(path=" + CONTACT_INBOX + "/*)" +
    "(resourceType=javamagazin/components/contactRequest))")
})
public class ContactRequestJobGenerator implements EventHandler {
  @Reference
  private JobManager jobManager;
  @Reference
  private ResourceResolverFactory resourceResolverFactory;

  @Override
  public void handleEvent(Event event) {
     Map<String, Object> properties = new HashMap<>();
     properties.put(JOB_CONTACT_REQUEST_PATH, event.getProperty(PROPERTY_PATH));
     jobManager.addJob(JOB_TOPIC_CONTACT_REQUEST, properties);
  }
}

Im Gegensatz zu Events haben Jobs eine Verarbeitungsgarantie und unterstützen explizit asynchrone Verarbeitung. Ein Job wird durch genau einen JobConsumer verarbeitet. Das Beispielprojekt zeigt die asynchrone Jobverarbeitung im ContactRequestNotifier (Listing 5). Dieser kann potenziell unzuverlässige Aufgaben – wie den Aufruf des REST-API eines anderen Systems – übernehmen und, falls dies fehlschlägt, erneut gestartet werden.

@Component
@Service
@Properties({
  @Property(name = PROPERTY_TOPICS, value = JOB_TOPIC_CONTACT_REQUEST)
})
public class ContactRequestNotifier implements JobConsumer {
  ExecutorService executorService = newSingleThreadExecutor();

  @Override
  public JobResult process(Job job) {
    final AsyncHandler handler = 
(AsyncHandler) job.getProperty(PROPERTY_JOB_ASYNC_HANDLER);

     // Perform whatever operation asynchronously
    executorService.submit(new Runnable() {
      @Override
      public void run() {
          handler.ok();
      }
    });

    return ASYNC;
  }

  @Deactivate
  protected void deactivate() {
    executorService.shutdownNow();
  }
}

Beide Services zeigen die typische Verwendung des Frameworks durch die Implementierung des Sling-API in OSGi Services.

Gute Aussichten für die Zukunft

Wer ein Framework einsetzen will, muss sicherstellen, dass es für einen absehbaren Zeitraum weiterentwickelt wird. Neue Releases sollten zudem keine Änderungen an den auf dem Framework basierenden Applikationen erzwingen. Hier kann Sling seine Stärken ausspielen: Mit Adobes Experience Manager (AEM) gibt es neben der vitalen Open-Source-Community eine große kommerzielle Kraft hinter dem Framework. Als Basisframework steht Sling hinter zahlreichen großen ECMS-Anwendungen, die von der Anpassbarkeit und Skalierbarkeit sowie der Stabilität der Framework-APIs profitieren.

Mit Jackrabbit Oak steht ab Sling 8 eine neue Implementierung des JCR-Standards zur Verfügung, die mit Fokus auf Performance und Skalierbarkeit – insbesondere für die Cloud – entwickelt wurde. Oak erlaubt unter anderem den Einsatz von MongoDB als Persistenzschicht und stattet somit Sling mit einem modernen Persistenzmechanismus aus.

Die Menge an Open-Source-Komponenten für Sling nimmt beständig zu. So steht mit NEBA eine Spring-Integration zur Verfügung, die den Einsatz von Sling insbesondere im EAI-Umfeld erleichtert. Sowohl Adobe als auch zahlreiche Dienstleister sind dazu übergegangen, Tools und auf Sling basierende Lösungen zu veröffentlichen.

Fazit

Sling ermöglicht die Erstellung zeitgemäßer REST-Applikationen und lässt seinen Anwendern dabei jede Menge Gestaltungsspielraum. Mit seiner starken Open-Source-Basis und den aktuellen Neuerungen – inbesondere Jackrabbit Oak – ist es ein interessanter Kandidat für anspruchsvolle Webprojekte.

Aufmacherbild: Grave von Shutterstock / Urheberrecht: Liudmyla Marykon

Verwandte Themen:

Geschrieben von
Olaf Otto

Olaf Otto arbeitet seit über fünfzehn Jahren im Bereich Softwarearchitektur mit den Schwerpunkten Enterprise Content Management, EAI und REST. Bei der Unic AG in Bern arbeitet er als Technischer Leiter und Softwarearchitekt. Er ist auf Twitter unter @deploynix erreichbar.

Daniel Takai

Daniel Takai ist Technologiemanager bei der Unic AG in Bern. Er ist dort für die Entwicklungsprozesse und Softwarearchitekturen verantwortlich. Er ist auf Twitter unter @danieltakai erreichbar.

Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: