Angular, wiederverwendbare Komponenten Teil 1/2

Komponenten sind das zentrale Konzept im Universum von Angular. Eine Komponente zu schreiben ist hier nicht schwer, die Herausforderung besteht in aller Regel darin Komponenten zu schaffen die möglichst viel Funktionalität so  kapseln das sie anderer Stelle wieder verwendet werde kann, dem „Nutzer“ aber den Freiraum lässt je nach Situation Anpassungen vornehmen zu können. Dieser Beitrag beschäftigt sich mit all den Möglichkeiten die Angular in diesem Kontext bietet.

ng-Content

Eine der leichteren Anforderungen ist es einen Bereich innerhalb der eigenen Komponente an zu bieten, in dem der Verwender eigene Inhalte platzieren kann. Ein typisches Beispiel wäre hier eine Layout-Komponete die einen bestimmten Rahmen vorgibt. Hierfür bietet Angular „ng-content“:

box-component.html

<div> <ng-content></ng-content></div>
<div> <ng-content select="[appBoxHeader]"></ng-content></div>
<div> <ng-content select=".footer"></ng-content></div>

In diesem Beispiel bietet die Komponente 3 Stellen an denen der Verwendet beliebige Inhalt einfügen kann. Die eindeutig Identifikation der Stellen wird durch Selektoren bewerkstelligt. In diesem Beispiel verwenden wir (von oben nach unten) eine Default-Stelle, eine Selektion über eine Direktive und eine Selektion über eine CSS-Klasse.

Die Verwendung dazu könnte wie folgt aussehen:

insert-children.component.html

  <app-box>
    <span>Hello Nr. 1</span>
    <span appBoxHeader>Hello Nr. 2</span>
    <span class="footer">Hello Nr. 3</span>
  </app-box>

ngTemplateOutlet

Einen Schritt weiter geht die Verwendung des ngTemplateOutlet. Hier können wir dem Anwender, auf Basis von HTML5 Templates, die optionale Möglichkeit geben bestimmte Teile unserer Komponente aus zu tauschen. Im folgenden Beispiel geht es darum eine sehr einfache Layout Komponente an zu bieten, die OutOfTheBox verwendet werden kann. Zusätzlich wollen wir dem Verwendet aber die Möglichkeit geben bei Bedarf bestimmte Teile mit eigenen Inhalten aus zu tauschen.

box-with-default.component.html

<div class="footer">
    <ng-template [ngTemplateOutlet]="footerTemplate"></ng-template>

© by GEDOPLAN, Dominik Mathmann</div>

Im Footer-Bereich unserer Komponente wollen wir eine standardisierte Copyright Information anzeigen, falls der Verwender keine eigenen Inhalte anbietet. In unserer Komponente sehen  wir dafür einen Bereich mittels „ng-template“ vor. Falls dieser Bereich vom Benutzer nicht mit „Leben gefüllt“ wird zeigen wir einen Standard-Text an. Sollte der Verwender ein entsprechendes Template übergeben sorgt die Direktive ngTemplateOutlet dafür das diese an der Stelle angezeigt wird. Die Referenz auf das individuelle Template (footerTemplate) lassen wir uns zu diesem Zweck in den Controller unserer Komponente injizieren:

box-with-default.component.ts

  @ContentChild("footer")
  footerTemplate: TemplateRef<any>;

Verwendung:

  <app-box-with-default title="Hello-Box">
       ...
       <ng-template #footer>
        <i>...my special footer...</i>
      </ng-template>
  </app-box-with-default>

Solche dynamischen Template-Bereiche können natürlich mehrfach in unserer Komponente vorkommen. Die Verknüpfung zwischen der Position innerhalb unserer Komponente und dem vom Verwender bereitgestellten Template geschieht über die Verwendung einer übereinstimmenden Template Variable (#footer > @ContentChild („footer“))

ngTemplateRef

Neben dem Austausch von festgelegten Template-Bereichen über TemplateRef gibt es auch Anwendungsfälle in denen wir innerhalb unserer Komponente eine Iteration durchführen wollen. Hier ist eine logische Anforderung, dass wir das Template der Iteration übergeben wollen, natürlich mit entsprechener Möglichkeit auf das aktuelle Objekt der Iteration zugreifen zu können.

entries.box.component.html

<div *ngIf="userTemplate">
  <ng-template ngFor [ngForOf]="elements" [ngForTemplate]="userTemplate">
  </ng-template></div>

Verwendung findet hier wieder das bereits bekannte ng-template mit einer vorrangestellten (optionalen) Prüfung ob einer individuelles Template überhaupt übergeben wurde. Die dann folgende Syntax mag auf den ersten Blick Verwirrend erscheinen. Hier wird ein Template verwendet, mit einer Direktive (ngFor) versehen um die Iteration zu ermöglichen. Mittels Property-Binding werden die Elemente definiert ([ngForOf]] und eine Lauf-Variable für das Element festgelegt (let-e). Diese Möglichkeit verwenden wir sehr oft, nur das Angular uns dafür mit seiner Microsyntax eine etwas einfachere Schreibweise anbietet. So entspricht die Verwendung von *ngFor beispielsweise im Hintergrund exakt dieser Synatx. So sind folgende Schreibweisen funktional völlig identisch:

	<li *ngFor="let e of entries"> Entry: {{e}}</li>
...
    <ng-template ngFor [ngForOf]="entries" let-e>
	<li>Entry: {{e}}</li>
</ng-template>

Die Verwendung der oben gezeigten Komponente ist dann wieder naheliegender:

Verwendung

  <ng-template let-entry>
<ul>
	<li><a (click)='alert(entry)'>{{entry.title}}</a></li>
</ul>
</ng-template>

Lediglich die Defintion der Laufvariablen mittels let-entry ist im Gegensatz zum vorrangegangenen Beispiel neu und dient logischerweise dazu Zugriff auf das aktuelle Iterationsobjekt zu bekommen.

Wie wir im ersten Teil sehen konnten bietet Angular mächtige Möglichkeiten Komponenten wiederverwenbar UND individualisierbar zu machen. Im zweiten Teil tauchen wir noch ein Stück tiefer und werfen einen Blick auf das dynamische Hinzufügen von Komponente mittels ViewContainerRef, ComponentFactory und ComponentFactoryResolver.

Beispiele zum kompletten nachlesen wie immer:

github.com/GEDOPLAN

Brauchen wir noch Java EE? (Oder: Magengrummeln auf der JAX)

Ich sitze gerade auf der JAX in Mainz und versuche mich vorsichtig wieder abzuregen. Aber der Reihe nach.

Auf Konferenzen hört man viele Buzz Words – da ist die JAX keine Ausnahme. Also hört man mehrere Zig Mal pro Tag die Worte Microservices und Container. Auch wenn die genaue Definition von ersterem nicht festgelegt ist, so meint man damit die Erstellung (oder auch Aufteilung) von Enterprise-Anwendungen in überschaubare Teile – im Sinne von DDD irgendetwas zwischen Aggregat und Bounded Context. Es handelt sich also um ein Pattern, das die Kopplung von Softwareteilen aus einem monolithischen Ansatz herauslöst und in die (Betriebs-) Infrastruktur verlagert. Dadurch gewinnt man zweifellos Flexibilität, kann also Anwendungsteile unabhängig voneinander deployen, unterschiedlich skalieren, ggf. sogar in unterschiedlichen Sprachen entwickeln. Das kommt natürlich nicht ohne Kosten wie Kommunikationsaufwand, Resilience, Schnittstellenevolution etc. Container in dem genannten Kontext meint in nahezu allen Fällen Docker, in aller Regel mit einer passenden Verwaltung und Orcherstrierung wie Kubernetes, OpenShift o.ä.

Das ist alles verständlich, spannend und nachvollziehbar. Was allerdings in meinem Augen ziemlich nervt, ist die allgegenwärtige Vermischung von Programm- und Betriebsmodell: Es erscheint als unumstößlliche Gewissheit, dass die alten, großen (und schlechten?)  monolithischen Anwendungen auf schwergewichtigen Java-EE-Servern betrieben werden, während man in einem Docker-Container nur leichtgewichtige (?) SE-Anwendungen laufen lassen kann, die mit allem anderen als Java EE entwickelt werden.

Dies wurde auch gerade in großen Teilen einer Konferenz-Session dargestellt, die den gleichen Titel trägt wie dieser Blog-Post. Ein paar Kernaussagen daraus:

  • „Kernbestandteile von Java EE sind Web (aka JSF), EJB, JCA, JMS. CDI ist als Bindeglied dabei“
    Das verkennt in meinen Augen nahezu komplett die Mächtigkeit von CDI. Dieser Standard/Container kann weit mehr als nur EJBs untereinander und mit der Web-UI zu verbinden. Über seine portable Extension-Schnittstelle ist eine fast grenzenlose Anpassung der gesamten Plattform möglich.
  • „Services baut man in Java EE mit EJB“
    Oder eben CDI, wenn man die speziellen Eigenschaften von EJB nicht benötigt (Timer, Remoting etc.). Wobei EJBs ja auch nicht schwergewichtig sind. Wer das behauptet, hat seit Java EE 5 (2009!) oder 6 nicht mehr drauf geschaut. Ein wesentlicher Punkt in Richtung Einfachheit ist doch Dependency Injection. Die kam für EJB mit der Version 3. Und seit 3.1 kann man auf Local Interfaces verzichten. Heute unterscheiden sich EJB und CDI Beans im Programmcode nur marginal und auch zur Laufzeit sind keine global signifikanten Vor- oder Nachteile zu verzeichnen.
  • „Mit Java-EE kann man nur auf relationale DBs zugreifen“
    Hä? Es gibt zwar kein Standard-API zum Zugriff auf NoSQL-DBs. Die sind ja ihrerseits auch noch gar nicht standardisiert. Aber man man doch das jeweilige proprietäre API benutzen!
  • „Die Struktur einer Java-EE-Anwendung ist starr und fix verknüpft, während in einem Container(-Cluster)  Services dynamisch verbunden und skaliert werden können“ (Man stelle sich dazu links eine Grafik eines Application Servers der Jahrtausendwende vor, rechts ein Schaubild einer aktuellen Kubernetes-Landschaft)
    Das vergleicht nun wirklich Äpfel mit Birnen (oder wie der Speaker es ausdrückte: Rüben mit Kraut). Das eine ist eine aus einem Build herausgefallene, monolithische Anwendung, das andere kleine Einzelservices in separaten Deployments. Warum sollten die Services auf der rechten Seite denn nicht mit dem Java-EE-Stack aufgebaut sein? Ein kleiner Service aus JAX-RS, CDI und JPA ist mit Java EE schnell geschrieben und läuft problemlos in einem Docker-Container. Auch der Footprint der Anwendung mit ca. 100 MB ist nicht wirklich schwergewichtig, auch wenn an der einen oder anderen Laufzeitumgebung vielleicht noch optimiert werden kann.
  • „Ihr müsst jetzt nicht auf Java EE verzichten, wenn eure Ansprüche an Durchsatz und Geschwindigkeit in einem niedrigen oder mittleren Bereich sind“
    Mal abgesehen davon, dass dieser Satz so ähnlich ist wie der, den man auf Sprüche-Karten findet („Das kannste schon so machen, aber dann isses halt Kacke“): Limitierungen in diesem Bereich liegen bspw. in synchronen Konzepten. So belegt ein JDBC-Treiber einen Thread, auch wenn er auf die Reaktion der DB wartet. Das lässt sich mit Aufteilung und Containern nicht verändern. Und im Web(-service)-Bereich bietet Java EE auch asynchrone Vorgehensweisen.

Ich will dem Speaker hier aber gar nicht unterstellen, dass er das so gemeint hat, wie ich (und nicht nur ich) das verstanden habe. Er hat nur den falschen Titel für seine Session gewählt: „Brauchen wir noch Java-EE-Server?“ wäre besser gewesen, denn in meinen Augen geht es ausschließlich um das damit verbundene Deployment- und Betriebskonzept. Das kann kann kritisieren, wenn man möchte. Allerdings läuft ein WildFly oder Payara auch wunderbar in einem Docker-Container – kaum unterscheidbar von einem alternativ darin liegenden Fat JAR.

Also: Tun wir uns allen den Gefallen, nicht die Spezifikation Java EE mit den darin enthaltenen Teilen JAX-RS, CDI etc. und die mögliche Ablaufumgebung Java-EE-Server zu vermischen.

Oder auch (mit einer kleinen Anleihe an eine bekannte Fernsehwerbung von früher): Nichts ist unmööööglich mit Java EE und Docker!

Angular Directive, Hostbinding

Directive sind neben Komponenten eines der zentralen Konzepte in Angular. Anders als Components bringt eine Directive kein eigenes Template mit, sondern erweitert die eigene Anwendung um Funktionalität.

Bevor wir zum eigentlich Hostbinding kommen, werfen wir noch mal einen Blick auf die Grundlagen einer Direktive:

Verwendung:

<div gedLoremIpsumContent>

Implementierung:

@Directive({
  selector: '[gedLoremIpsumContent]',
})
export class LoremIpsumContentDirective {}

Syntaktisch unterscheidet sich eine Direktive kaum von einer Component, analog müssen wir auch hier einen Selektor verwenden mit dessen Hilfe wir unsere Directive später im Template vewenden können. In diesem Fall haben wir uns für einen Attributs-Selector entschieden. Alternativ hätten wir hier auch auf einen Tag-Selector („gedLoremIpsumContent“) oder einen CSS-Selector („.gedLoremIpsumContent“) setzen können.

Das oben gezeigte Beispiel funktioniert, ohne sichtbaren nutzen, tadellos und erzeugt für das DIV das unsere Directive verwendet auch eine entsprechende Objektinstanz. Innerhalb unserer Direktive können wir nun Einfluss auf das DOM-Element nehmen auf dem unsere Direktive verwendet wird (der so genannten „Host“). Hierzu stehen uns 4 Möglichkeiten zur Verfügung.

ElementRef

Die Klasse „ElementRef“ bietet uns eine entsprechende Schnittstelle an um auf das zugrunde liegende HTML-Element zugreifen zu können. Nach der Injektion über den Konstruktor liefert die so erhaltene Instanz mittels der Methode „nativeElement“ den Zugriff auf die DOM-API. Hierüber sind wir nun in der Lage die gewünschten Manipulationen am Element vorzunehmen.

  constructor(element: ElementRef) {
    element.nativeElement.style.cursor = 'pointer';
  }

Der direkte Zugriff auf die DOM API sollte jedoch nur im äußersten Notfall verwendet werden. Angular ist so konzipiert das der Browser nur eines der möglichen Anzeige-Medien ist. Hier sind auch native Clients, mobile Geräte und serverseitiges Rendering möglich, die keinen zugrundeliegenden DOM-Baum haben und auch lässt sich eine Direktive die sich direkt auf das native Element stützt nicht per Unit-Tests testen.

Renderer

Der Renderer ist im Grunde eine Abstraktion für den direkten Zugriff auf die DOM-API und sollte immer vorrangig, vor der direkten Verwendung des „nativeElement“ , verwendet werden. Diese Klasse bietet diverse Methoden an um das zugrunde liegende DOM-Element zu manipulieren.

  constructor(element: ElementRef, renderer:Renderer2) {
    renderer.setStyle(element.nativeElement, "border", "1px solid black");
  }

Render2-API

HostBinding

Das HostBinding bietet uns die Möglichkeit das Attribut von unserem Host (in diesem Beispiel dem DIV-Element) direkt an eine eine Property zu binden, ohne das wir uns im weiteren Programm-Verlaufen darum kümmern müssen das Änderungen des Wertes auch am DOM-Elemente aktualisiert werden.

  @HostBinding('innerHTML')
  innerHTML: string;

  ngOnChanges() {
   this.innerHTML = 'Hello World';
  }

Alternativ zur (empfohlenen) Verwendung des @HostBinding Decorators kann das Binding auch bei der Deklaration der Directive gesetzt werden:

@Directive({
  selector: '[gedLoremIpsumContent]',
  exportAs: 'loremIpsumContent',
  host: {
    'class': 'demo',
    '(mouseenter)': 'mouseEnter()'
  }
})

HostListener

HostListener bieten uns analog zum HostBinding Zugriff auf Events die auf dem Host auftreten können. Dazu verwendenden wir den  @HostListener-Decorator an eine unserer Methoden und legen fest welches Event verarbeitet werden  und welche Parameter übergeben werden sollen

  @HostListener('click', ['$event'])
  onHostClick($event) {
   this.innerHTML="New Value";
  }

Mit diesen Möglichkeiten bietet Angular alles um sicher und einfach direkt oder indirekt mit den Elementen des DOM-Baumes zu arbeiten. Das Ganze als Live-Beispiel gibt es wie immer in

Github: https://github.com/GEDOPLAN/angular-directive

Angular, Lifecycle Methoden

Mittels Lifecycle Methoden bietet Angular uns die Möglichkeit an bestimmten Stellen des Lebenszyklus einer Komponente ein zu greifen um z.B. Initialisierungen vor zu nehmen. Dieser Beitrag stellt die unterschiedlichen Methoden kurz vor.

demo.png

Die Implementierung von Lifecycle Methoden ist denkbar einfach. Es genügt innerhalb der eigenen Komponente eine entsprechende Methode zu implementieren und Angular sorgt dafür das diese aufgerufen wird. Bei der Arbeit mit TypeScript ist es allerdings empfehlenswert das entsprechende Interface zu verwenden, sodass der Compiler bereits  darauf aufmerksam machen kann das die gewünschte Lifecycle Methode eventuell nicht oder nicht korrekt implementiert ist.

import {Component, OnInit,} from '@angular/core';

@Component({...})
export class BoxComponent implements OnInit {

  ngOnInit() {...}

  ngOnChanges() {...}
}

Werfen wir einen Blick auf die von Angular zur Verfügung gestellten Methoden:

constructor
Keine „echte“ Lifecycle Methode von Angular, hier sollte möglichst wenig Logik implementieren werden. Die primäre Aufgabe innerhalb des Konstruktors besteht darin Dependency Injection zu steuern und zu verarbeiten

ngOnInit
Dient zu Initialisierung der eigener Komponente. An dieser Stelle sind eventuell vorhandene @Input-Parameter bereits vorhanden. Grundlegende Initialisierungen gehören in diese Methode.

ngOnChange
Diese Methode wird einmalig nach der ngOnInit-Methode aufgerufen und dann bei jeder Änderung der Input-Parameter. Müssen die Input-Parameter zur Initialisierung unserer Komponente in irgendeiner Art und Weise verarbeitet werden (z.B. eine Datums-Komponente erhält einen Date-String, arbeitet intern aber mit einem Date-Objekt) sollten diese Aufgaben nicht in der ngOnInit Methode, sondern hier implementiert werden.

ngAfterContentInit
Initialisierung der @ContentChildren-Komponenten. Die View-Query zum auffinden der mittels @ContentChildren annotierten Kind-Komponenten wird vor dieser Methode durchgeführt, sodass alle Content- (nicht aber die View-)-Children zur Verfügung stehen

ngAfterViewInit
Initialisierung der @ViewChildren-Komponenten. Die View-Query zum auffinden der mittels @ViewChildren annotierten Objekte wird vor dieser Methode durchgeführt, sodass alle nun auch alle View-Children zur Verfügung stehen

ngOnDestroy
Wird aufgerufen wenn die Komponente aus dem DOM-Baum entfernt wird, z.B. bei einem Routing oder Ausblenden über *ngIf.

ngAfterContentChecked | ngAfterViewChecked.
Angular Change Detection Methoden, kann Verwendet werden um den Lebenszyklus/ChangDetection zu protokollieren

ngDoCheck
Methode in der eine eigene Change Detection implementiert werden kann. Diese Methode sollte nur in Ausnahmefällen verwendet werden und mit möglichst wenig Logik versehen werden, da diese Methode sehr häufig aufgerufen wird. Ein Beispiel wann so etwas nötig ist: die „ngOnChange“ Methode wird nur dann bei Änderungen der Input Parameter aufgerufen wenn sich der Parameter selber ändert, wird hier ein Objekt verwendet und es ändert sich lediglich ein Attribut des Objektes wird die ngOnChange Methode nicht aufgerufen.

Demo-Projekt @ Github

JAAS – ein eigenes Login Modul

JAAS steht für Java Authentication and Authorization Service und bietet uns im Java EE Umfeld unter anderem die Möglichkeit einen standardisierten Mechanismus zu verwenden um einen Benutzer zu autorisieren. Der Wildfly 10 bringt eine ganze Reihe solcher Login Module mit, bietet uns aber auch die Möglichkeit ein solches Modul selber zu schreiben.

Die Liste der zur Verfügung stehenden Möglichkeiten einen Benutzer zu autorisieren ist im Wildfly 10 schon recht umfangreich. Angefangen von einfachen .properties-Dateien, über relationale Datenbanken und Zertifikate bis hin zu LDAP-Abfragen ist alles möglich. Sollten wir jedoch in einem konkreten Fall mit diesen Mitteln nicht auskommen, z.B. weil die Login Informationen in einem bestehenden System über eine Schnittstelle zur Verfügung gestellt werden, lässt der Standard uns auch ein eigenes solches Modul implementieren.

Implementierung

Beginnen wir bei der Implementierung eines solches Moduls. Diese Klasse muss das Interface javax.security.auth.spi.LoginModul implementieren und somit folgende Methoden zur Verfügung stellen:

  • initialize(…) – Initialisierung des Modules
  • login() – Login Prüfung
  • logout() – führt einen Logout durch
  • commit() – Methode wird nach erfolgreichem Login aufgerufen
  • abort() – Methode wird bei nicht erfolgreichem Login aufgerufen

Auffällig ist das die „login“ Methode keinerlei Parameter annimmt, wie es vielleicht zu erwarten wäre. Der Zugriff auf diese, vom Benutzer je nach Auth-Method unterschiedlich zur Verfügung gestellte, Eingabe erhält man über die Verwendung so genannter Callback-Methoden. Die Prüfung auf einen gültigen Login erfolgt dann ganz nach eigenen Wünschen und führt im Erfolgsfall dazu, dass ein Principal dem aktuellen Subject (Aufrufer) hinzugefügt wird

public boolean login() throws LoginException
{
NameCallback nameCallback = new NameCallback("Benutzer");
PasswordCallback passwordCallback = new PasswordCallback("Password", false);

try {
    callbackHandler.handle(new Callback[]{nameCallback, passwordCallback});
    String username = nameCallback.getName();
    String password = new String(passwordCallback.getPassword());

    if (logInWS(username, password)) {
    loggedIn = true;
    identity = new UserPrincipal(username);
    subject.getPrincipals().add(identity);
    subject.getPublicCredentials().add(identity);
    return true;
    }
}
catch (IOException e) {
    e.printStackTrace();
}
catch (UnsupportedCallbackException e) {
    e.printStackTrace();
}

return false;
}

Nachdem die Login Methode erfolgreich durchlaufen wurde, folgt der Aufruf der Methode „commit“ in der in aller Regel die für den Principal gültigen Rollen aufgebaut werden. Hierfür werden entsprechende Implementierungen von java.security.acl.Group und java.security.Principal benötigt. Diese sind sehr leicht selber zu schreiben (s. Github) oder wir bedienen im Fall von JBoss bei „Picketbox“ (Security Implementierung des Wildfly, Maven:org.picketbox/jbosssx-bare ).

public boolean commit() throws LoginException
{
if (loggedIn) {
    Group group = new SimpleGroup("Roles");
    SimplePrincipal role = new SimplePrincipal("super-user");
    group.addMember(role);
    subject.getPrincipals().add(group);
    return true;
}
return false;
}

Konfiguration

Damit das Ganze vom Wildfly und unserer Anwendung auch erkannt und verwendet wird, sind noch einige Konfigurationen notwendig. So muss eine entsprechende Security Domain im Wildfly konfiguriert werden. Hier wird neben der Implementierungs-Klasse auch ein entsprechender Namen vergeben. Die Implementierung muss in diesem Fall innerhalb der Anwendung zur Verfügung stehen. Alternativ dazu kann das Login Modul auch als WildflyModul bereitgestellt werden, dann muss lediglich das Attribut „module“ beim „login-module“ mit entsprechendem Namen des Moduls ergänzt werden.

standalone.xml

...
<security-domain name="MySecurityDomain" cache-type="default">
    <authentication>
        <login-module code="de.gedoplan.custom.jaas.CustomJAASLoginModule" flag="required"/>
    </authentication>
</security-domain>
...

Schlussendlich muss die im Beispiel verwendete Webanwendung mit EJB-Komponenten mit entsprechenden Jboss-Konfigurationen versehen werden, sodass unser angelegte Security Domain auch Verwendung findet

jboss-web.xml

<jboss-web>
    <security-domain>java:/jaas/MySecurityDomain</security-domain>
    <default-encoding>UTF-8</default-encoding>
</jboss-web>

jboss-ejb3.xml

<jboss:jboss
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:s="urn:security:1.1"
    version="3.1" impl-version="2.0">
 
    <assembly-descriptor>
        <s:security>
            <ejb-name>*</ejb-name>
            <s:security-domain>MySecurityDomain</s:security-domain>
        </s:security>
    </assembly-descriptor>
</jboss:jboss>

Ein vollständiges Beispiel wie immer bei uns im Github

Docker Orchestrierung mit Kubernetes

Docker bietet uns eine leichtgewichtige, containerbasierte Form der Virtualisierung, welche sich gut dafür eignet, Anwendungen zu paketieren und auszuliefern. Dabei wird das Verwalten der Images mit Hilfe von Dockerfiles und Registries schon gut von Docker direkt unterstützt und gestaltet sich sehr einfach. Etwas mehr Aufwand ist bei der Verwaltung der Container selber zu Laufzeit notwendig. Zwar bietet Docker hier natürlich entsprechende Möglichkeiten für das mitgeben von Parametern beim Containerstart, was so direkt aber nicht die Wiederverwendbarkeit der Laufzeitkonfiguration unterstützt und sehr viel Eigeninitiative bei Themen wie Networking etc. erfordert. Docker-Compose kann uns hier bereits einiges an Arbeit abnehmen, wobei auch hier noch mehr Unterstützung für die alltäglichen Aufgaben bei Entwicklung und Betrieb der Anwendungen wünschenswert wäre.

Es gibt eine Reihe von Plattformen die uns bei der sogenannten Orchestrierung von Dockercontainern unterstützen. Diese Technologien ermöglichen uns unter anderem die Container in einer geclusterten Umgebung auszuführen, wobei die Anzahl Instanzen der Container ganz einfach zur Laufzeit beliebig erhöht werden kann. Die Plattform übernimmt dabei das Hochfahren und Verteilen der Container in dem Cluster. Darüber hinaus werden viele weitere Features geboten, wie z.B. wiederverwendbare Konfiguration, Loadbalancing, Rolling Updates, Mechanismen für Austausch von Passwörten/Zertifikaten und Servicediscovery. Im folgendem wird nun Kubernetes als eine Plattform zur Docker Orchestrierung vorgestellt unter dem Hauptgesichtspunkt, wie uns diese Vorgehensweise bei dem Entwickeln und Betreiben von eigenen Anwendungen unterstützen kann.

Kubernetes

Kubernetes ist eine ursprünglich von Google entwickelte opensource Plattform für das Clustermanagement und Betreiben dockerbasierter Applikationen. Es zeichnet sich durch seine deklarativ gehaltene Art der Konfiguration aus und erfreut sich mittlerweile auch schon einer recht großen Verbreitung. Als grundlegende Technologie wird Kubernetes von einigen Cloud-Anbietern genutzt, wie z.B. Openshift und Google Containerengine.

Kubernetes ist im Grunde gemäß dem Master-Slave-Prinzip aufgebaut. Ein Cluster setzt sich also zusammen aus einem Master und einer Reihe von Nodes. Der Master bietet die API Schnittstelle an und ist für das Verwalten der Nodes und das Hochfahren und Aufteilen der Container auf diese zuständig (scheduling). Die Nodes benötigen jeweils einen Docker-Runtime und sind für das tatsächliche Starten der Dockercontainer zuständig. Auf diese Weise ist ein elastisches Clustering möglich, dass es ermöglicht, bei Bedarf neue Nodes hinzuzufügen, welche dann vom Master genutzt werden können um Containerinstanzen zuzuweisen.

API

Die Schnittstellen für die Kommunikation mit dem Kubernetes Cluster werden per REST bereitgestellt. Das Kommandozeilenprogramm kubectl kann hierfür als komfortabler Client verwendet werden. Es gibt in Kubernetes verschiedene Arten von Objekten, die angelegt und Konfiguriert werden können. Alle diese Objekte können in Form von YAML Files definiert und über die Schnittstellen eingespielt werden. Dadurch, dass alle Konfigurationsobjekte durch deskriptive YML Strukturen beschrieben werden können, ist es einfach diese Konfigurationen wiederzuverwenden und in einem Versionsverwaltungssystem wie Git zu unterzubringen.

Pods

Ein Pod ist das grundlegendste Objekt in Kubernetes und kapselt einen (oder mehrere) Docker Container. Als Einheit für das Scheduling innerhalb von Kubernetes werden enstprechend Pods verwendet und nicht Dockercontainer direkt. Man kann sich einen Pod als Wrapper um einen Container vorstellen. Zwar ist es möglich mehrere Container in einem Pod unterzubringen, allerdings wird nach der am meist verbreiteten Vorgehensweise immer ein Pod für einen Container genutzt. Es handelt sich bei einem Pod um eine nicht trennbare Einheit, kann also immer nur komplett auf einem Clusterknoten laufen. An Konfiguration enthält ein Pod einen Verweis auf das damit Verknüpfte Dockerimage und als weitere Parameter Hauptsächlich Angaben zu Volumes, Umgebungsvariablen und Ports. Also im Großen und Ganzen das, was ansonsten als Information beim Docker run Befehl mitgegeben wird.

Beispiel für einen Pod:

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: demoapp
spec:
  containers:
  - image~: demoapp:1
    name: demoapp
    ports:
      - containerPort: 3306
        protocol: TCP

Dieses Konfigurationsobjekt würden wir jetzt im Falle einer eigenen Anwendung einfach im Git-Repository des Projektes mit einchecken. Über einen Befehl kann es eingespielt werden:

kubectl create –f mypod.yml

Metadata: Labels + Selectors und Annotations

Den Objekten wie Pods können in Kubernetes Metadaten angehängt werden, sogenannte Labels und Annotations. Dabei Handelt es sich um Key-Value-Paare. Objekte können mit Hilfe dieser Labels durch Selector-Queries gefunden werden. Dies ist wichtig, da im Cluster ja viele verschiedene oder auch mehrere Instanzen einer Podkonfiguration laufen können. Über diese Selektor-Queries ist es möglich bestimmte Elemente im Cluster nach bestimmten Kriterien zu finden. Zum Beispiel könnte dies für Discovery genutzt werden, ein Pod könnte ein bestimmtes Label haben um zu kennzeichnen, dass metrische Daten per REST bereitgestellt werden. Der Value von einer Annotation könnte dann die URL sein. Ein Werkzeug für das Sammeln der Daten könnte sich so jetzt alle Anwendungen im Cluster heraus suchen, die metrische Daten anbieten.

Services

Wenn nun mehrere Anwendungen miteinander kommunizieren sollen, z.B. unsere Anwendung mit einer MySQL Datenbank, so brauchen wir dafür eine Schnittstelle, über welche diese Applikation zentral angesprochen werden kann. Direkt mit dem Pod Verbinden geht nicht, da wir in einer geclusterten Umgebung sind und nicht wissen, wo der Pod läuft und außerdem soll es ja später mal möglich sein, mehrere Instanzen eines Pods zu betreiben. Diese zentralen Schnittstellen werden über sogenannte Services abgebildet. Ein Service besteht aus einem Namen und ein oder mehreren Ports unter welchem der Zugriff auf die Anwendung dahinter Clusterweit möglich ist. Der Service-Endpoint ist dabei auch in der Lage ein Loadbalancing durchzuführen, es könnten also mehrere Instanzen der Anwendung dahinter laufen. Die Pods die mit diesem Service verknüpft sind werden mit Hilfe einer Selector-Query festgelegt, benötigen also entsprechende Metadaten in der Podkonfiguration.

apiVersion: v1
  kind: Service
  metadata:
    name: demoapp
  spec:
    selector:
      name: demoapp
    ports:
    - port: 8080
      protocol: TCP

Der Service wird auf einer IP im gesamten Cluster bereitgestellt, die mit dem Namen des Service aufgelöst werden kann. Die Adresse des Service kann in anderen Pods aus Umgebungsvariablen, die automatisch von Kubernetes gesetzt werden, ausgelesen werden:

DEMOAPP_SERVICE_HOST=10.0.0.11

DEMOAPP_SERVICE_PORT=8080

Services sind zunächst nur innerhalb des Cluster erreichbar, können aber auch nach außen hin sichtbar gemacht werden. Dies wird über den Service Typ festgelegt. Auf einer Cloud-Plattform wie Openshift besteht hier die einfache Möglichkeit über Type:LoadBalancer eine IP über einen externen LoadBalancer der Plattform zu bekommen. Services können auch genutzt werden, um Schnittstellen zu externe Anwendungen für Applikationen innerhalb des Clusters anzubieten.

Dies bedeutet, wenn wir eine eigene Anwendung betreiben, welche auf eine Mysql Datenbank angewiesen ist, müssten wir zwei Services bereitstellen. Die Datenbank mit einem normalen Service Clusterintern, die eigentliche Anwendung mit einem Service der eine Adresse extern bereitstellt. Die Anwendung kann jetzt über die Umgebungsvariablen an die Adresse für die Datenbank gelangen.

ReplicationControllers / ReplicaSets

ReplicationControllers erlauben es auf einfach Art und Weise mehrere Instanzen einer Podkonfiguration zu betreiben. Der ReplicationController stellt dabei sicher, dass immer eine gewünschte Anzahl der Pods im Cluster läuft. Erzeugt werden die Pods vom Controller dabei aus einem Pod-Template. Es kann eine gewünschte Anzahl Replicas direkt in der Konfiguration angegeben werden, diese kann dann später aber auch zur Laufzeit geändert werden.

apiVersion: v1
  kind: ReplicationController
  metadata:
    name: mysql-1
  spec:
    replicas: 1
    selector:
      name: mysql
    template:
      apiVersion: v1
      kind: Pod
      metadata:
        labels:
          name: mysql
      spec:
        containers:
        - image~: mysql
          name: mysql
          ports:
            - containerPort: 3306
              protocol: TCP

Wenn es nun einen Service gibt, der per Selector die Pods einsammelt, so sind alle Replicas die der Controller hochfährt mit diesem Service verknüpft.

ReplicaSets sind eine neuere Variante der ReplicationController in Kubernetes, die im Großen und Ganzen die gleichen Möglichkeiten bieten, aber zusätzlich neuere Selector-Features nutzen können.

Deployments

Die Funktionalität eines ReplicationControllers/ReplicaSets ist für das einfache Betreiben und Ausrollen einer Anwendung noch ein wenig zu low-level. Für diese Aufgabe gibt es daher spezielle Objekte, die sogenannten Deployments. Von der grundlegenden Konfiguration her sieht das Objekt so aus wie ein ReplicationController, aber ermöglicht uns darüber hinaus weitere Funktionalität.

Wenn wir eigene Anwendungen betreiben brauchen wir eine Möglichkeit, einfach eine neue Version auszurollen. Dies bedeutet im Dockerumfeld in der Regel, dass wir eine neue Version des Images erstellt haben. Nun müssen die alten Container gestoppt und die neuen gestartet werden. Ein Deployment ermöglicht genau dies über einen einfachen Befehl:

kubectl set image deployment/demoapp-deployment demoapp=demoapp:1.1

Nicht nur das Umstellen der Imageversion, auch andere Änderungen an der Deploymentconfig (außer Replica Scaling) bewirken einen Rollout. Dabei wird eine Änderungshistorie geführt, sodass es immer möglich ist, im Problemfall auf eine frühere Version des Deployments zurückzurollen.

Für das Ausrollen werden mehrere Verfahren angeboten, hier ist vor allem die Strategie RollingUpdate interessant, die uns ermöglicht ein Update ohne Ausfallzeit durchzuführen. Dabei würden zunächst die Pods mit dem neuen Image gestartet und erst anschließend wenn diese erfolgreich hochgefahren wurden, werden die alten Pods angehalten. Dabei bleibt immer die gewünschte Mindestanzahl Replicas verfügbar.

Ob ein Pod erfolgreich gestartet wurde wird zunächst davon abhängig gemacht, ob der Container ohne Fehler gestartet wurde und noch läuft. Interessant ist hier aber natürlich auch, ob die Anwendung schon läuft, denn erst dann sollen ja die alten Instanzen gestoppt werden. Dies ist über Angabe einer entsprechenden livenessProbe möglich. Hierfür kann entweder ein Command oder eine URL angegeben werden, mit welchem die Verfügbarkeit der Anwendung getestet werden kann.

Für unsere Anwendung bedeutet das nun, dass wir für diese ein Deployment definieren und immer wenn wir eine neue Version gebaut haben und das Dockerimage in die Registry gepushed ist, ein Rollout mit dem neuen Imagetag auf dem Deployment antriggern.

Persistent Volumes

Daten werden wie in Docker gewohnt in Volumes abgelegt, welche in den Pod Konfigurationen den Containern zugeordnet werden können. Genutzt werden können hier viele verschiedene Arten von Volumes, angefangen von einem Pfad auf dem Node, einem Netzwerklaufwerk über verschiedene Cloudspeicherlösungen bis hin speziellen Lösungen wie Git-Repositories.

Zusätzlich bietet Kubernetes ein eigenes Konzept bestehend aus speziellen Objekten, den PersistenVolumes und PersistenVolumeClaims. PersistentVolumes sind dabei Objekte die vom Administrator eingetragen werden und einen Zugriff auf einen Speicher (auch hier verschiedene Typen möglich) mit einer bestimmten Größe definieren.

apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: mysqlvolume
  spec:
    capacity:
      storage: 2Gib
    accessModes:
      - ReadWriteOnce
    hostPath:
      path: /data/volumes/mysql
    persistentVolumeReclaimPolicy: Retain

Auf der anderen Seite legen die Nutzer dieses Speichers ein PersistentVolumeClaim Objekt an, welches dann über Selektoren mit einem PersistentVolume Verknüpft wird. Diese Claims können dann wie normale Volumes in den Pods eingebunden werden.

 apiVersion: v1
  kind: PersistentVolumeClaim
  metadata:
    name: mysql
  spec:
    accessModes:
      - ReadWriteOnce
    resources:
      requests:
        storage: 2Gib
    volumeName: mysqlvolume
kind: Pod
metadata:
  labels:
    name: mysql
spec:
  containers:
  - image~: mysql
    name: mysql
    ports:
      - containerPort: 3306
        protocol: TCP
    volumeMounts:
      - mountPath: /var/lib/mysql
        name: storage
  restartPolicy: Always
  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: mysqlvolume

Die Idee hier ist also, dass das tatsächliche bereitstellen des Speichers eine Aufgabe der Administration ist und die Anwender sich über die Claims diese bereitgestellten Volumes anfordern, ohne über die technischen Hintergründe des dahinterliegenden Speichermediums informiert sein zu müssen.

Secrets

Falls mehrere Anwendungen miteinander kommunizieren sollen, müssen in der Regel Passwörter, Tokens und Zertifikate ausgetauscht werden. Kubernetes bietet hierfür die sogenannten Secrets als spezielle Objekte an. In einem Secret werden die Daten in Key-Value Form gespeichert. Diese Secrets können nun entweder als Volume in die Pods eingebunden werden, oder Werte aus ihnen können dem Pods als Umgebungsvariable übergeben werden. Kubernetes selbst hängt in alle Pods Secrets für den Zugriff auf die Cluster API als Volume ein, sodass aus den Containern heraus Anfragen an die Kubernetes API möglich sind.

Definieren eines Secrets:

apiVersion: v1
kind: Secret
metadata:
  name: mysqlsecret
type: Opaque
data:
  password: ZGVtb3Bhc3N3b3Jk
  username: ZGVtb3VzZXI=

Einbinden als Umgebungsvariable:

containers:
          - name: demoapp
            image~: "demoapp:1"
            ports:
              - containerPort: 8080
                protocol: "TCP"
            env:
              - name: DB_SCHEMA
                valueFrom:
                  secretKeyRef:
                    name: mysqlsecret
                    key: database

Entsprechend könnten Secrets eingesetzt werden um z.B. die Zugriffsdaten für die Datenbank bereitzustellen. So müssen sie nicht in der Anwendung hart kodiert sein, sie müssen noch nicht einmal bekannt sein. Die Anwendung bindet sich einfach das Secret ein und holt sich die Werte daraus für die Anmeldung an der Datenbank. Verschiedene Passwörter für Produktions- und Testdatenbank sind somit einfach zu handhaben.

ConfigMaps

Ähnlich wie die Secrets aber etwas allgemeiner, für nicht sensible Daten gehalten, sind die Configmaps. Diese bieten die Möglichkeit zentrale Konfiguration anzubieten, welche dann von den Pods konsumiert werden kann. Dadurch, dass die Konfiguration so in die Plattform ausgelagert wird und nicht direkt in den Container-Images verdrahtet ist, sind die Anwendungsimages besser portabel/wiederverwendbar.

Namespaces

Kubernetes bietet uns zu Zwecken der Isolation an, sogenannte Namespaces zu definieren. Die können dafür genutzt werden, um mehrere voneinander getrennte Umgebungen innerhalb des Clusters zu definieren. Objekte werden immer in einem Namespace angelegt wo sie zunächst Zugang zu den anderen Objekten dieses Namespaces haben (Zugriffe auf andere Namespaces sind auch möglich). Wenn eine Anwendung einen Service über seinen Namen anspricht, so wird der Service aus demselben Namespace angezogen. Man kann sich das auch bei sonstigen Selector-Queries oder Zuordnungen zunutze machen, z.B. PersistentVolumes, Secrets, Configmaps usw.

Wofür die Namespaces sich daher gut einsetzen lassen ist also das Bereitstellen mehrerer Umgebungen wie z.B. test, qualitätsicherung und produktion. Wenn die zu dem Projekt gehörenden Objekte alle entsprechend portabel geschrieben sind, kann also eine neue Umgebung aufgesetzt werden in dem ein neuer Namespace erstellt wird und alle Objekte in diesem angelegt werden. Wodurch auch das duplizieren sehr komplexer, aus mehreren Anwendungen bestehenden, Umgebungen mit einigen Befehlen umzusetzen ist.

Templates

Wenn es darum geht, eine Umgebung, die aus vielen verschiedenen Objekten besteht, in einem Schritt einzuspielen, können Templates von großer Hilfe sein. Ein Template wird definiert in einem YAML File, welches beliebig viele Kubernetes Objekte enthalten darf. Es ist möglich Parameter für das Template zu definieren und diese Parameter in der Konfiguration der Objekte zu verwenden. Das Template kann nun entweder direkt aus dem File heraus angewendet, oder im Cluster für die einfache Wiederverwendung eingespielt werden. Damit ist es dann möglich über anwenden des Template alle darin enthaltenen Objekte auf einen Schlag in einem Namespace einzuspielen (Unter eventueller Angabe der benötigten Parameter).

Weiteres

Es gibt darüber hinaus noch viele weitere Features in Kubernetes. Zugriff auf Ressourcen, Namespaces, oder Aktionen im Cluster kann beschränkt werden. Anwendungen können sich über ServiceAccounts am API Server anmelden so wir normale Benutzer und haben dann Aktionen gemäß der ihnen zugeteilten Rollen zur Verfügung.

Automatisches horizontales anpassen der Replicas anhand von metrischen Daten, wie CPU Nutzung, ist möglich

Cron Jobs können definiert werden, die um eine bestimmte Zeit einen Prozess anstoßen, der die für die Durchführung notwendigen Pods hochfährt, welche nach abarbeiten der Aufgabe wieder gestoppt werden.

Fazit

Kubernetes bietet uns viele Funktionen, die für den Betrieb containerbasierter Anwendungen hilfreich sind. Alle Objekte können zusammen in einem Template mit der Anwendung als YAML Datei mit in das Repository eingecheckt werden. Mit dem Template können mehrere gleichartige Umgebungen aufgesetzt werden. Neue Images können einfach ausgerollt werden mit Hilfe der Deployments. Viele Instanzen der Anwendung können parallel gestartet werden und ein elastisches Clustering wird ermöglicht. Es gibt außerdem Mechanismen für Service-Discovery und das Verwalten von zentraler Konfiguration und Secrets.

Für das einfache Ausprobieren von Kubernetes auf einem lokalen Rechner empfiehlt sich Minikube, welches einen 1Knoten-Cluster(Master+Node) in einer virtuellen Maschine wie VirtualBox startet.

Als Alternative zur direkten Nutzung von Kubernetes kann auch Openshift empfohlen werden. Dies ist eine opensource Cloudplattform welche auf Kubernetes aufbaut und dieses um einige Features erweitert. Zum Beispiel ist es hier einfach Möglich Services über Routen nach außen hin sichtbar zu machen, dies wird von dem enthaltenen externen Loadbalancer übernommen.

Zusammenfassung Vorgehensweise für Anwendungsentwicklung

Als Beispiel gehen wir von eine einfachen Anwendung bestehend aus einer Java-Webanwendung und einer Mysql Datenbank aus.

Wir benötigen ein Docker-Image für die Datenbank und eines welches als Basis für unsere Anwendung dient (eventuell mit ApplicationServer).
Desweiteren benötigen wir nun :

Einen ReplicationController für die Datenbank und einen Service der die Datenbank innerhalb des Clusters zur Verfügung stellt. Für die eigene Anwendung nutzen wir ein Deployment mit der RollingUpdate-Strategie. Diese Anwendung wird über einen Service nach draußen zur Verfügung gestellt. Auf die Datenbank greift die Anwendung über die entsprechenden Umgebungsvariablen für den Service zu. Zugangsdaten für die Datenbank werden als Secret bereitgestellt und in den Pod der Anwendung eingebunden. Als Volume wird ein PersistentVolumeClaim in dem Pod für die Datenbank eingebunden. Alle diese Objekte werden zusammen in ein Template verpackt und mit der Anwendung eingecheckt. Nun können mit Hilfe von Namespaces verschiedene Stages für Test und Produktion erzeugt werden. In diesen müssen jeweils die PersistentVolumes angelget werden, sowie eventuell die Secrets für die Datenbank (Sofern die unterschiedlich sein müssen.). Anschließend kann das Template genutzt werden um die Umgebung in den Namespace einzuspielen.

Für das Bereitstellen einer neuen Version in einer der Umgebung muss nun das Dockerimage der Anwendung gebaut und getaggt (und in eine Registry gepushed) werden. Danach wird über einen Aufruf der Kubernetes API ein erneutes Ausrollen des Deployments mit dem neuen Imagetag angestoßen.

WebSocket mit Java EE und JavaScript

Die Spezifikation (rfc6455) zu Websockets hat schon einige Jahre auf den Buckel, ist damit aber in allen bekannten Browsern verfügbar. Die Javascript API ist übersichtlich und mit nur wenigen Schritten kann eine bidirektionale Verbindung zwischen Browser und Server hergestellt werden, um Benachrichtigungen vom Server zu erhalten ohne ein alt modisches Polling zu implementieren. Auf Java-Seite steht mit Java EE 7 ebenfalls ein Standard bereit (jsr356) der die Server-Seite für Websocket Szenarien bereit stellt. Werfen wir einen kurzen Blick auf das Zusammenspiele im Beispiel JavaScript + Java EE 7

Die Initialisierung einer Websocket Verbindung in JavaScript ist mit einigen wenigen Zeilen Code bereitgestellt und stellt  Listener-Methoden bereit um auf Ereignisse zu reagieren

var wsConnection = new WebSocket('ws://example.ws.service/ws');
wsConnection.onopen = function () {...};
wsConnection.onerror = function (error) {...};
wsConnection.onmessage = function (e) {...};

Auch das Pendant auf der Serverseite ist nicht wesentlich umfangreicher, das Vorgehen ist simple: wir erstellen eine Klasse und registrieren sie als WebSocket Endpoint mittels der Annotation „ServerEndpoint“. Die entsprechenden Nachrichten um auf Events zu reagieren werden mit zusätzlichen Annotation versehen. In unserem Beispiel führen wir zusätzlich eine Liste aller verbundenen Clients um diese mit Push-Nachrichten zu versorgen.

EchoEndpoint.java

@ServerEndpoint("/echo")
public class EchoEndPoint {

    private static Set<Session> userSessions = Collections.newSetFromMap(new ConcurrentHashMap<Session, Boolean>());

    @OnOpen
    public void onOpen(Session userSession) {
	System.out.println("Neue Verbindung aufgebaut...");
	userSessions.add(userSession);

    }

    @OnClose
    public void onClose(Session userSession) {
	System.out.println("Verbindung getrennt...");
	userSessions.remove(userSession);

    }

    @OnMessage
    public void onMessage(String message, Session userSession) {
	broadcast(message);

    }

    public static void broadcast(String msg) {
	System.out.println("Broadcast Nachricht an alle:" + msg);
	for (Session session : userSessions) {
	    session.getAsyncRemote().sendText("Re: " + msg);
	}

    }
}

 

websocket_demo

 

Das war es schon, mehr ist nicht nötig um JavaScript und Java EE 7 über einen WebSocket zusammen zu bringen.

Ein vollständiges Beispiel findet sich wie immer bei uns im Github:

websocket-demo @ GITHUB