Die wichtigsten Neuerungen in der Java Enterprise Edition 7

Seite 3: Concurrency, WebSockets, JMS, Batch

Inhaltsverzeichnis

Mit den Concurrency Utilities lassen sich über eine einfache API konkurrierende Aktionen von Anwendungskomponenten ausführen, ohne die Container-Integrität zu gefährden. Die direkte Verwendung der Concurrency APIs von Java SE in Java-EE-Containern ist verboten, weil sich diese der Ressourcenverwaltung durch die Container entziehen und zu Stabilitätsproblemen führen. Daher sind die Concurrency Utilities nicht
mehr als eine Java-EE-konforme Erweiterung des JSR 166 in Java SE. Die vorhandenen Funktionen verwaltet nunmehr der Container. Statt eines java.util.concurrent.ExecutorService gibt es jetzt das Enterprise-Gegenstück javax.enterprise.concurrent.ManagedExecutorService. Eine Standard-Instanz steht via JNDI (Java Naming and Directory Interface) bereit, sie lässt sich aber auch direkt injizieren:

@Resource(lookup="DefaultManagedExecutorService")
ManagedExecutorService executor;

Die auszufĂĽhrende Aufgabe wird als Task bezeichnet und implementiert dabei das Runnable- oder Callable-Interface.

public class MeineAufgabe implements Runnable {
@Override
public void run() {
//...
}
}

Der Task wird dann ĂĽber den ManagedExecutorService ausgefĂĽhrt:

Collection tasks = new ArrayList();
tasks.add(new MeineAufgabe());
executor.invokeAny(tasks);

Neben dem Task ohne Rückgabewert gibt es noch den Callable, der einen Rückgabewert haben kann. CDI Beans können ebenfalls Tasks sein. Hierbei sollte allerdings auf den Scope geachtet werden. Nur @Application oder @Dependent Scope lassen sich als Tasks verwenden. Mit javax.enterprise.concurrent.ManagedScheduledExecutorService kann der Entwickler zeitgesteuerte Aufgaben ausführen. Der Aufruf erfolgt analog zum ExecutorService, einzig die Angabe der relevanten Zeiten ist noch zusätzlich erforderlich.

ScheduledFuture<?> f = executor.schedule(new MeineAufhabe (), 15,
TimeUnit.SECONDS);

Der Code führt die Aufgabe mit 15 Sekunden Startverzögerung aus. Über den sogenannten Container Context werden Funktionen von Anwendungskomponenten wie Classloader, Namespaces und Security bereitgestellt.

Wichtiger Teil heutiger Webtechniken, die in die Java EE Einzug gehalten haben, sind die WebSockets. Als bidirektionales Kommunikationsprotokoll über TCP erlauben sie die direkte Kommunikation zwischen Server und Client. Dabei sind die WebSockets ein Request for Comments (RFC) der Internet Engineering Task Force (IETF), die HTML-5-fähige Browser auch mit zugehöriger JavaScript API ausgestalten. Im Gegensatz zu HTTP wird eine TCP-Verbindung dabei persistent aufrechterhalten. Der derzeitige Stand des JSR 356 bildet nicht alle Möglichkeiten ab, sondern fokussiert sich auf die serverseitige Bereitstellung der WebSocket Endpoints, die Entgegennahme von Text-, Binär- und Kontroll-Nachrichten, die Lebenszyklus-Events sowie die Integration in Java EE Security. Ein einfacher Endpunkt für einen Chat sieht so aus:

@ServerEndpoint("/chat/{room}")
public class ChatServer {
@OnMessage
public void receiveMessage(String message, @PathParam("room") String room) {
//...
}
}

Die mit @onMessage annotierte Methode wird beim Empfang einer Nachricht eines Clients verwendet. Je nach Nachrichtenformat wird über den Methodenparameter die Behandlung von Textnachrichten via String, Binärnachrichten via byte[] und Streams via InputStream ermöglicht. Mit @PathParam lassen sich primitive Java-Typen oder Strings im Server-Endpoint verwenden. Neben @OnMessage gibt es noch die Lebenszyklus-Methoden (@OnOpen, @OnClose und @OnError), die sich ausimplementieren lassen. CDI wird vollständig unterstützt. Alternativ zu den Annotationen lässt sich ein Server Endpoint auch via API komplett programmatisch erstellen und somit auch in Java SE bereitstellen. Im Idealfall ist ein im Browser ausgeführter JavaScript-Client die Gegenstelle für den Server.

var websocket = new WebSocket("ws://localhost:8080/chatapp/chat");

Aber auch Java-Klassen können Client sein. Dafür wird die @ClientEndpoint-Annotation angeboten:

@ClientEndpoint
public class ChatClient {
//...
}

Hier stehen wieder die Lebenszyklus-Events mit den entsprechend annotierten Methoden analog zum Server zur Verfügung. Mit Encodern und Decodern können Entwickler analog zu JAX-RS applikationsspezifische Datentypen serialisieren. Leider können sie hier nicht einfach Provider registrieren, sondern müssen sie dem Server Endpoint mitgegeben.

@ServerEndpoint(value = "/personservice",
encoders = {PersonEncoder.class},
decoders = {PersonDecoder.class})

Die bekannten Security-Constraints aus dem web.xml Deployment Descriptor greifen auch fĂĽr WebSocket Endpoints.

Nach gut neun Jahren Stillstand lässt sich die neue Version der Java Message Services (JMS) als die umfangreichste Veränderung bezeichnen. Durch die Anwendung der Mittel aus Java SE] 7 wie "Try-with-resources", aber auch dank Dependency Injection ist die Verwendung der API schlank und einfach geworden. So wurden Connection- und Session-Objekte im neuen JMSContext zusammengefasst. Statt mit zweien arbeiten zu müssen, reicht jetzt der neue Context, der auch injiziert werden kann. Er ist also weder explizit zu erstellen noch zu schließen.

@Inject JMXContext context;

Die Vereinfachung der APIs hat man aber noch weiter vorangetrieben. So lässt sich dank neuer Producer-Methoden jetzt flüssiger arbeiten. Das Senden einer JMS-Nachricht auf dem neuen Weg schrumpft deutlich:

TextMessage textMessage = context.createTextMessage(body);
context.createProducer().setPriority(1).setProperty("foo",
"bar").send(demoQueue, textMessage);

Dabei sind die Producer "leichtgewichtig" und können bei Bedarf erzeugt werden. Die Notwendigkeit, sie zwischenzuspeichern, entfällt damit komplett. Überhaupt ist ein Nachrichtenobjekt nicht mehr zu instanziieren. Mit JMS 2.0 lassen sich die gewünschten Nutzdaten dem Producer direkt übergeben:

context.createProducer().send(demoQueue,"Hallo iX");

Andersherum entfällt beim Empfangen nunmehr eine Typumwandlung auf eine genaue Klasse.

JMSConsumer consumer = context.createConsumer(demoQueue);
String result = "Empfangen " + consumer.receiveBody(String.class, 1000);

Mit den Änderungen hat man die Arbeit stark vereinfacht und eine Menge überflüssigen Code verhindert. Dabei wirken sich die Änderungen in diesem Punkt nicht nur auf die Arbeit in der Java-EE-Spezifikation, sondern auch auf Java SE aus. Im Java-EE-Container erfordern die Neuerungen den Einsatz eines Resource-Adapters, was aber für die Entwickler unbemerkt die Server erfüllen sollten.

Es hat lange gedauert, bis Batch-Anwendungen auch in die Java Enterprise Edition Einzug gehalten hat. Stark angelehnt an die Konzepte von Spring Batch und unter der Regie von IBM steht der JSR 352 bereit zur Batch-Verarbeitung. Die Grundidee ist die Verarbeitung einer Serie von Jobs, die ohne Interaktion Massendaten in langlaufenden Schritten verändern. Ein Job ist dabei das Hauptobjekt. Es untergliedert sich in einzelne Schritte ("steps"), die nach bekanntem Batch-Pattern (Lesen, Bearbeiten, Schreiben) Aufgaben ausführen. Gestartet werden Batch-Jobs über den sogenannten Batch-Operator:

JobOperator jo = BatchRuntime.getJobOperator();
Properties jobParams = new Properties();
jobParams.put("file.url", " testdata.txt");
long jobId = jo.start("extract-cities", jobParams);

Im obigen Beispiel wird dem Batch-Job noch Properties mitgegeben. Dies wird zusammen mit dem Namen der eigentlichen Batch-Beschreibung an die start-Methode transferiert. Unter dem ĂĽbergebenen Namen findet sich unter META-INF/batch-jobs/ ein gleichnamiges XML-Dokument, das den Job beschreibt.

<job id="extractCity" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
<step id="populateCities" >
<chunk>
<reader ref="entryReader" />
<processor ref="entryProcessor"/>
<writer ref="entryWriter"/>
</chunk>
</step>
</job>

Dabei beschreiben job, chunk und step den elementorientierten Bearbeitungstyp. Mit einem sogenannten Batchlet lässt sich auch der aufgabenorientierte Bearbeitungstyp umsetzen. Die drei hauptsächlichen Klassen werden aus dem XML mit ihrem Bean-Namen angesprochen. Demzufolge ergibt sich, dass für Reader, Processor und Writer jeweils eine entsprechende Klasse umzusetzen ist:

@Named("entryReader")
public class EntryReader extends AbstractItemReader {
//...
@Override
public String readItem() {
//...
}

@Named("entryProcessor")
public class EntryProcessor implements ItemProcessor {
//...
@Override
public Object processItem(Object o) throws Exception {
//...
}

@Named("entryWriter")
public class EntryWriter extends AbstractItemWriter {
//...
@Override
public void writeItems(List<Object> list) throws Exception {
//...
}