Entenhausen an der Börse

JAX-RS 2.0 und WebSocket

Bernhard Löwenstein
© Shutterstock/isak55

Die Java-EE-7-Plattform kann mit einigen zusätzlichen JAX-RS-Features dienen und stellt mit WebSocket eine neue, interessante Technologie bereit. Der folgende Artikel zeigt anhand eines praktischen Beispiels, bei dem ein Börsenkursdienst für die Bewohner von Entenhausen entwickelt werden soll, wie sie sich einsetzen lässt.

Im ersten Teil dieser vierteiligen Serie (im Java Magazin 10.2014) haben wir uns die wesentlichen Änderungen und Neuerungen der siebten Ausgabe der Java Enterprise Edition im Überblick angesehen. Im aktuellen Artikel geht es um die praktische Nutzung dieser Technologien. Anhand eines konkreten

Beispiels schauen wir uns folgend das Client-API und die Filter von JAX-RS 2.0 an. Weiterhin setzen wir WebSocket ein, um über am Server geänderte Daten im Client umgehend informiert zu werden.

Nachdem mein Patenkind David in den letzten Monaten zum großen Entenhausen-Fan mutiert ist, blieb mir nichts anderes übrig, als mich ebenfalls damit zu beschäftigen. Damit das umfassende Studium dieser fiktiven Welt inklusive seiner Charaktere nicht ganz umsonst war, wollen wir folgendes Szenario für unser Beispiel wählen: Stellen Sie sich vor, die Bewohner von Entenhausen rufen eine Börse ins Leben, um ihren Bewohnern einen Marktplatz für den Wertpapierhandel zu bieten. Jeder Entenhausener kann mit seinem Unternehmen an der Börse notieren. Dagobert Duck Enterprises (DAGO), die Panzerknacker AG (PANZ) und die Daniel Düsentrieb Inc. (DANI) nutzen diese Möglichkeit sofort – jeder allerdings mit einem anderen Hintergedanken: Die reichste Ente der Welt hofft durch den Börsengang noch viel reicher zu werden, die Panzerknacker wollen die Börsengeschäfte zum Geldwaschen nutzen und Entenhausens größter Erfinder möchte so seine Forschungsprojekte finanzieren. Es gilt nun im ersten Schritt, einen Dienst zu implementieren, über den sich der aktuelle Börsenkurs eines jeden Unternehmens abfragen lässt. Auch das Setzen eines Kurses muss möglich sein. Später soll man sich in Echtzeit über Kursänderungen informieren lassen können.

Da der Börsenkursdienst auch direkt aus Applikationen, die ausschließlich im Browser laufen, nutzbar sein soll, werden wir einen RESTful Web Service implementieren. Sobald diese Lösung steht, ergänzen wir sie um einen WebSocket-Endpunkt, um Kursänderungen live nachverfolgen zu können.

RESTlos glücklich dank neuer Features

Da wir auf der grünen Wiese beginnen, können wir technologisch aus dem Vollen schöpfen. So setzen wir den WildFly 8.1.0 in der finalen Version als Applikationsserver ein und werden die neuen Java-8-Features nutzen. Ausrollen werden wir unsere Dienste in Form einer klassischen Java-Enterprise-Webapplikation. Die Clientprogramme setzen wir in Form eines eigenen Java-Projekts um. Zum Speichern der Börsenkurse nutzen wir die als Singleton ausgeführte Session Bean MarketPriceStore (Listing 1). Im Grunde bauen wir damit einen klassischen Key-Value Store nach. Als Schlüssel wird die Unternehmenskennung verwendet, als Wert der aktuelle Börsenkurs. Die Daten werden nicht persistent gehalten. Dieses Feature folgt erst in einem späteren Sprint. Der Store bietet dem Entwickler mit store() und retrieve() die klassischen Methoden zum Speichern und Abfragen von Daten. Zwei Besonderheiten sind zu beachten: Ändert sich durch das Speichern der Börsenkurs eines Unternehmens, so wird ein CDI-Event abgefeuert. Alle, die solche Ereignisse überwachen, bekommen die Unternehmenskennung und den neuen Kurswert in Form eines ChangeEvent-Objekts übermittelt. Außerdem liefert die Methode retrieve() nicht null zurück, wenn kein Börsenkurswert gefunden wird, sondern verwendet Optional<Double> als Rückgabetyp. Diese mit Java 8 neu eingeführte Klasse stellt eine Art Wrapper um ein Objekt dar und bietet verschiedene Methoden, dank der sich komfortabel mit Objekten arbeiten lässt, die möglicherweise leer sind.

... @Singleton @LocalBean public class MarketPriceStore { private Map<String, Double> map = new HashMap<String, Double>(); @Inject Event<ChangeEvent> eventSource; public void store(String companyId, Double marketPriceNew) { Double marketPriceOld = map.get(companyId); if (!Objects.equals(marketPriceOld, marketPriceNew)) { map.put(companyId, marketPriceNew); eventSource.fire(new ChangeEvent(companyId, marketPriceNew)); } } public Optional<Double> retrieve(String key) { return Optional.ofNullable(map.get(key)); } } 

Zur Erstellung des RESTful Web Service SimpleStockMarketResource (Listing 2) nutzen wir mit JAX-WS klarerweise die zugehörige Standardtechnologie der Enterprise-Plattform. Den Store zur Datenhaltung lassen wir uns injizieren. Als HTTP-Methoden unterstützen wir PUT und GET, wobei sich mit PUT nicht nur Börsenwerte aktualisieren, sondern auch anlegen lassen. In der Methode retrieve() delegieren wir die eigentliche Abfrage an unseren selbstimplementierten Key-Value Store. Da dieser einen optionalen Rückgabetyp hat, können wir ohne explizite null-Überprüfung und mithilfe eines Lambdas recht elegant den Statuscode setzen. Damit der Web Service tatsächlich ausgerollt wird, müssen wir in web.xml noch ein Servlet mit dem Namen javax.ws.rs.core.Application eintragen. Unter jboss-web.xml tragen wir als Kontextwurzel entenhausen ein. Danach kann es mit dem Deployment auch schon losgehen. Unser RESTful Web Service wird sogleich auf dem lokalen Rechner mit WildFly-Standardkonfiguration unter http://localhost:8080/entenhausen/resources/simplemarketservice verfügbar gemacht. Wollen wir nun über unseren Service den Börsenkurs von Dagobert Duck Enterprises anlegen oder ändern, so brauchen wir lediglich die Unternehmenskennung DAGO an den vorherigen URL anhängen und den Börsenkurswert als Inhalt des PUT Requests mitschicken. Zur Abfrage reicht das Absetzen eines GET Requests unter Verwendung des erweiterten URL. Für die einfache Nutzung bauen wir nun anschließend einen Webclient und ein Kommandozeilenwerkzeug.

... @Path("/simplestockmarket") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN) public class SimpleStockMarketResource { @Inject private MarketPriceStore marketPriceStore; @PUT @Path("/{companyid}") public void store(@PathParam("companyid") String companyId, Double marketPrice) { companyId = companyId.toUpperCase(); marketPriceStore.store(companyId, marketPrice); } @GET @Path("/{companyid}") public Response retrieve(@PathParam("companyid") String companyId) { companyId = companyId.toUpperCase(); ResponseBuilder responseBuilder = Response.status(Status.NOT_FOUND); marketPriceStore.retrieve(companyId).ifPresent( marketPrice -> responseBuilder.status(Status.OK).entity(marketPrice) ); return responseBuilder.build(); } } 
<web-app ...> <servlet-mapping> <servlet-name>javax.ws.rs.core.Application</servlet-name> <url-pattern>/resources/*</url-pattern> </servlet-mapping> </web-app> 

Beim webbasierten REST-Client (Listing 4) handelt es sich um eine HTML-Datei, die Ajax zur Kommunikation mit dem Service nutzt. Zwecks Erleichterung greifen wir auf die JavaScript-Bibliothek jQuery zurück. Über die Webseite (Abb. 1) lassen sich die Börsenkurse setzen und abfragen.

... <script> $(document).ready(function() { $("#get").click(function() { var companyId = $("#companyid").val(); $.ajax({ url: "http://localhost:8080/entenhausen/resources/simplestockmarket/" + companyId, type: "GET", success: function(value) { $("#marketprice").val(value); } }); }); $("#set").click(function() { var companyId = $("#companyid").val(); var marketPrice = $("#marketprice").val(); $.ajax({ url: "http://localhost:8080/entenhausen/resources/simplestockmarket/" + companyId, type: "PUT", contentType: "text/plain", data: marketPrice, success: function(value) { alert("SUCCESS: Set!"); } }); }); }); </script> ... 

Abb. 1: Über den webbasierten REST-Client lassen sich die Börsenkurse setzen und abfragen

Gleiche Funktionalität bietet das kommandozeilenbasierte Werkzeug SimpleStockMarketResourceClient (Listing 5). Die erforderlichen Parameter werden beim Aufruf mit übergeben:

SimpleStockMarketResourceClient -get <companyid> SimpleStockMarketResourceClient -set <companyid> <marketprice> 

Innerhalb der Java-Klasse nutzen wir das neue Client-API. Hierzu müssen wir allerdings eine JAX-RS-2.0-kompatible Implementierung als Bibliothek einbinden. Unsere Wahl fällt auf RESTEasy, da es auch im WildFly selbst genutzt wird. Nachdem das Design der Clientprogrammierschnittstellen gut gelungen ist, sind die entsprechenden Codestellen hoffentlich selbsterklärend.

... public class SimpleStockMarketResourceClient { private static final String URI = "http://localhost:8080/entenhausen/resources/simplestockmarket"; public static void main(String[] args) throws Exception { try { String operation = args[0]; String companyId = args[1]; if (operation.equalsIgnoreCase("-get")) { Double marketPrice = ClientBuilder.newClient() .target(URI) .path("{companyid}") .resolveTemplate("companyid", companyId) .request() .get(Double.class); System.out.println("SUCCESS: Get! => [Market Price=" + marketPrice + "]"); } else if (operation.equalsIgnoreCase("-set")) { Double marketPrice = Double.valueOf(args[2]); Response response = ClientBuilder.newClient() .target(URI) .path("{companyid}") .resolveTemplate("companyid", companyId) .request() .put(Entity.text(marketPrice)); if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) { System.out.println("SUCCESS: Set!"); } else { System.err.println("ERROR: Set! => [Status Code=" + response.getStatus() + "]"); } } } catch (Exception e) { printUsage(); } } ... } 

Es folgt der Moment, in dem der Frosch ins Wasser hüpft. Soll heißen: Lassen Sie uns die beiden Clients ausprobieren. Und siehe da – keiner von beiden funktioniert. Der Grund ist aber schnell ausgemacht: Da wir die Clients lokal starten, findet aus Serversicht ein Cross-Origin Resource Sharing statt. Diese Problematik lässt sich einfach umgehen. Der RESTful Web Service muss lediglich mit jedem Response das Header-Feld Access-Control-Allow-Origin mit Wert * mitschicken. Hier erlebt nun ein weiteres neues JAX-RS-Feature seine große Stunde: der Filter. Derartige Filter lassen sich server- und clientseitig sowie für Requests und Responses setzen. Wir benötigen einen serverseitigen Antwortfilter, dem wir den Namen ResponseFilter (Listing 6) geben. Wir deaktivieren dort auch gleich das Caching. Das Elegante an solchen Filtern ist, dass sich darin die klassischen Querschnittsbelange unterbringen lassen, ohne dass die eigentliche Geschäftslogik verwässert wird. Es reicht aus, einen JAX-RS-Filter mit @Provider zu annotieren. Der Container wendet ihn dann automatisch auf alle RESTful Web Services aus dem gleichen Deployment an. Nachdem wir unsere Webapplikation neu ausgerollt haben, probieren wir erneut unseren Dienst aus – und plötzlich gelingt das Anlegen und Abfragen von Börsenkursen.

... @Provider public class ResponseFilter implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext request, ContainerResponseContext response) throws IOException { response.getHeaders().add("Access-Control-Allow-Origin", "*"); CacheControl cacheControl = new CacheControl(); cacheControl.setNoCache(true); response.getHeaders().add("Cache-Control", cacheControl); } } 

Aufmacherbild: Financial data on a monitor. Finance data concept. von Shutterstock / Urheberrecht: isak55

[ header = Seite 2: Stets topaktuell informiert dank WebSocket ]

Stets topaktuell informiert dank WebSocket

Im zweiten Schritt werden wir nun einen Dienst entwickeln, von dem wir nach der Kontaktaufnahme den aktuellen Börsenkurs zum gewünschten Unternehmen übermittelt bekommen und der uns in Folge über alle Kursänderungen informiert, bis wir die Lust verlieren und die Verbindung schließen. Die Java-EE-7-Plattform bringt dazu erstmals eine passende Technologie mit: das Java API for WebSocket. Ein WebSocket-Endpunkt ist ein entsprechend annotiertes POJO, in dem der Entwickler auf die Ereignisse Open, Message, Error und Close reagieren kann. Als Event Handler dienen dabei entsprechend annotierte Methoden. Über deren Parameter werden die benötigten Daten wie die Session oder die eigentliche Nachricht übergeben. Das Besondere an WebSocket-Endpunkten ist, dass jeder kontaktaufnehmende Client seine eigene, private Instanz am Server laufen hat.

Unser WebSocket-Endpunkt SimpleStockMarketWSServerEndpoint (Listing 7) registriert bei der Verbindungsaufnahme die zugehörige Session beim Singleton EventListener (Listing 8). Dem Session-Objekt kommt große Bedeutung zu, denn darüber lassen sich Daten an den aufrufenden Client zurückschicken. Der selbstimplementierte Listener folgt dem Observer Pattern und übernimmt folgende Aufgabe: Sobald der Börsenkurs eines Unternehmens im Key-Value Store geändert wird, sendet der Listener per WebSocket-Protokoll eine Nachricht mit dem aktuellen Börsenkurs an alle Clientendpunkte, die in ihrer initialen Nachricht die zugehörige Unternehmenskennung angegeben haben. Die Änderung eines Börsenwerts bekommt der Listener dabei mittels des CDI-Event-Mechanismus mit. Dank des in Java 8 hinzugekommenen Streaming API lässt sich das Filtern und Iterieren über die passenden Sessions elegant anschreiben. Zum eigentlichen Nachrichtenversand wird auf eine Hilfsmethode zurückgegriffen, die die Exception von session.getBasicRemote().sendObject(marketPrice) abfängt und nicht an den Aufrufer weitergibt. Zum Filtern wird die benutzerdefinierte Property companyId ausgewertet, die vom Serverendpunkt in der Methode message() für jede Session gesetzt wird. Der aufrufende Client muss hierfür in seiner initialen Nachricht die Unternehmenskennung als Inhalt mitschicken. Sofern zu dem Unternehmen Daten vorliegen, wird dem aufrufenden Client gleich der aktuelle Börsenkurs mitgeteilt. Schließt der Client die Verbindung, so deregistriert der WebSocket-Endpunkt die zugehörige Session vom Listener.

... @ServerEndpoint("/websockets/simplestockmarket") public class SimpleStockMarketWSServerEndpoint { @Inject private MarketPriceStore marketPriceStore; @Inject private EventListener listener; @OnOpen public void open(Session session) { listener.register(session); } @OnMessage public void message(Session session, String companyId) { companyId = companyId.toUpperCase(); session.getUserProperties().put("companyId", companyId); marketPriceStore.retrieve(companyId) .ifPresent(marketPrice -> Common.sendMarketPrice(session, marketPrice)); } @OnClose public void close(Session session) { listener.unregister(session); } } 
... @Singleton @LocalBean public class EventListener { private List<Session> sessions = new ArrayList<Session>(); public void register(Session session) { sessions.add(session); } public void unregister(Session session) { sessions.remove(session); } public void receiveEvent(@Observes ChangeEvent event) { sessions.stream() .filter(session -> Objects.equals(session.getUserProperties().get("companyId"), event.getCompanyId())) .forEach(session -> Common.sendMarketPrice(session, event.getMarketPrice())); } } 

Nachdem wir die Webapplikation neu ausgerollt haben, steht der WebSocket-Dienst zur Verfügung. Nun fehlt lediglich mehr ein Client, mittels dessen man sich für ein bestimmtes Unternehmen registrieren kann und so lange über die aktuellen Börsenkurse informiert wird, bis die Verbindung geschlossen wird. Auch hier wollen wir wieder einen Webclient und ein Kommandozeilenwerkzeug implementieren. Der Webclient (Listing 9) ist vom GUI her fast identisch mit dem früheren. Wir nutzen auch hier wieder jQuery. Klickt jemand auf den Registrierungsknopf, wird die Verbindung zum WebSocket-Endpunkt aufgebaut. Klickt der Benutzer auf den Deregistrierungsknopf, wird die Verbindung geschlossen. Um auf das Open-, Message-, Error- und Close-Ereignis reagieren zu können, sieht das API den klassischen JavaScript-Event-Handler-Mechanismus vor. Wesentlich für uns ist, dass wir beim Open-Event die initiale Nachricht mit der Unternehmenskennung an den Serverendpunkt schicken und dass beim Eintreffen einer Nachricht der Börsenkurs im Frontend aktualisiert wird.

... <script> var websocket; $(document).ready(function() { $("#register").click(function() { websocket = new WebSocket("ws://localhost:8080/entenhausen/websockets/simplestockmarket"); websocket.onopen = function() { alert("SUCCESS: Registered!"); websocket.send($("#companyid").val()); } websocket.onmessage = function(evt) { $("#marketprice").val(evt.data); }; websocket.onclose = function() { alert("SUCCESS: Unregistered!"); } }); $("#unregister").click(function() { websocket.close(); }); }); </script> ... 

Das Kommandozeilenwerkzeug besteht aus zwei Klassen: SimpleStockMarketWSClient (Listing 10) und SimpleStockMarketWSClientEndpoint (Listing 11). Zum Aufruf dient die erstgenannte Klasse, der man als Parameter die Unternehmenskennung mitgibt:

SimpleStockMarketWSClient <companyid> 

In ihr wird der Clientendpunkt initialisiert und danach über den clientseitigen WebSocket-Container die Verbindung zum serverseitig laufenden Endpunkt aufgebaut. Die Verbindung bleibt so lange offen, bis der Nutzer eine beliebige Taste drückt. Was mit den vom Serverendpunkt übermittelten Börsenkursen konkret gemacht werden soll, lässt sich bei der Instanziierung des eigentlichen Clientendpunkts als Parameter mitgeben. Nachdem dort ein Consumer erwartet wird, lassen sich natürlich auch Lambdas und Methodenreferenzen verwenden. Der Clientendpunkt folgt konzeptuell dem Serverendpunkt. Als Entwickler muss man lediglich mittels entsprechend annotierter Methoden auf die relevanten Events reagieren. Über das Session-Objekt innerhalb eines Clientendpunkts lassen sich jederzeit Nachrichten an den serverseitig laufenden Dienst schicken.

... public class SimpleStockMarketWSClient { private static final String URI = "ws://localhost:8080/entenhausen/websockets/simplestockmarket"; public static void main(String[] args) throws Exception { try { String companyId = args[0]; SimpleStockMarketWSClientEndpoint clientEndpoint = new SimpleStockMarketWSClientEndpoint(companyId, System.out::println); WebSocketContainer container = ContainerProvider.getWebSocketContainer(); container.connectToServer(clientEndpoint, new URI(URI)); System.in.read(); clientEndpoint.disconnect(); } catch (Exception e) { printUsage(); } } ... } 
... @ClientEndpoint public class SimpleStockMarketWSClientEndpoint { String companyId; Consumer<String> consumer; Session session; public SimpleStockMarketWSClientEndpoint(String companyId, Consumer<String> consumer) { this.consumer = consumer; this.companyId = companyId; } @OnOpen public void open(Session session) throws Exception { this.session = session; session.getBasicRemote().sendText(companyId); } @OnMessage public void message(String value) { consumer.accept(value); } public void disconnect() { try { session.close(); } catch (Exception e) { e.printStackTrace(); } } } 

Zum Testen gehen wir wie folgt vor: Wir legen über einen der REST-Clients die drei Unternehmen DAGO, PANZ und DANI inklusive Börsenwert 123.0 an. Nun öffnen wir die beiden WebSocket-Clients und registrieren uns für Dagobert Ducks Firma. Als aktueller Börsenkurs wird 123.0 angezeigt. Danach ändern wir über einen der REST-Clients den DAGO-Kurs auf 987.0. Sogleich erscheint dieser Börsenkurs auch im Browser und auf der Kommandozeile. Dieses Spielchen funktioniert so lange, bis wir uns deregistrieren.

Fazit und Ausblick

Dank der Ergänzungen, die JAX-RS in der Version 2.0 erfahren hat, sollten nun kaum mehr Wünsche offen sein. Das Client-API ist gut gelungen und sehr intuitiv nutzbar. Besonders überraschend ist das nicht, hat man bei dessen Entwicklung doch lediglich im Best-Practice-Stil von bereits vorhandenen Implementierungen abgekupfert. Mit WebSocket steht in der Java Enterprise Edition 7 nun endlich eine Technologie zur Verfügung, die es Serverkomponenten ermöglicht, Daten quasi in Echtzeit an webbasierte Clients zu schicken. Die Implementierung von WebSocket-Endpunkten ist, sobald man das Konzept verstanden hat, recht einfach und weist technische Eleganz auf. Weitere Änderungen und Neuerungen in Java EE 7 werden wir uns im nächsten Teil ansehen. Hoffen wir, dass bis dahin weder Dagobert Duck noch die Panzerknacker die Entenhausener Börse gesprengt haben, denn sonst muss wieder einmal ein Rettungsschirm her.

Geschrieben von
Bernhard Löwenstein
Bernhard Löwenstein
Bernhard Löwenstein (bernhard.loewenstein@java.at) ist als selbstständiger IT-Trainer und Consultant für javatraining.at tätig. Als Gründer und ehrenamtlicher Obmann des Instituts zur Förderung des IT-Nachwuchses führt er außerdem altersgerechte Roboterworkshops für Kinder und Jugendliche durch, um diese für IT und Technik zu begeistern.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: