Da Remote Method Invocation (RMI) ein Java-spezifisches Protokoll ist, kann es nicht als programmiersprachenunabhängiger Standard eingesetzt werden. Als allgemeiner Begriff für entfernte Methodenaufrufe zur Interprozesskommunikation hat sich Remote Procedure Call (RPC) durchgesetzt. Der Begriff RPC ist mit dem RFC 707 im Jahr 1976 lange vor dem Entstehen von RMI geprägt worden. Die anfänglichen RPC-Implementierungen sind inzwischen veraltet. Daher beschreibt der Begriff RPC heute eher die grundlegende Idee eines Methodenaufrufs auf Objekten in anderen Adressräumen und weniger ein konkretes Protokoll oder eine konkrete Implementierung.
Eine konkrete RPC-Implementierung ist hingegen das XML-basierte Protokoll SOAP, das seit 2000 vom W3C als empfohlener Standard etabliert worden ist. Wie wir noch im Detail sehen werden, definiert SOAP ein XML-Schema zur Repräsentation der auszutauschenden Daten in XML und nutzt die üblichen Protokolle des TCP/IP-Modells zur Nachrichtenübertragung, auf oberster Ebene i.d.R. HTTPS. Ursprünglich war SOAP ein Akronym für Simple Object Access Protocol. Seit der aktuellen Version 1.2 soll SOAP aber nicht mehr als Abkürzung, sondern als Eigenname verstanden werden, da es subjektiv nicht unbedingt einfach (simple) ist und nicht ausschließlich dem Zugriff auf Objekte (object access) dient. Historisch geht SOAP auf das Protokoll XML-RPC zurück, das Microsoft und Dave Winer 1998 veröffentlicht haben.
Im Folgenden soll das Bankkontoservice-Anwendungsbeispiel aus dem vorherigen Kapitel zu RMI in einen SOAP-Webservice überführt werden. Dabei soll die fachliche Funktionaliät des Anwendungsbeispiels möglichst nicht verändert werden. Die einzelnen Methoden des Webservice sind:String createAccount(String: owner)
: In dieser Methode wird ein neues Bankkonto für den angegebenen Eigentümer erzeugt. Die eindeutige Id des Kontos (vom Typ java.util.UUID
) wird in ihrer String-Repräsentation zurückgegeben. Die Klasse java.util.UUID
selbst kann nicht als Input- oder Return-Argument verwendet werden, da die Klasse keinen leeren Default-Konstruktor besitzt, der für die Objekterzeugung per Reflection API erforderlich ist.void addAccountEntry(String id, AccountEntry entry)
: In dieser Methode wird einem Bankkonto ein neuer Kontoeintrag (AccountEntry
) hinzugefügt. Ein Kontoeintrag besteht weiterhin aus einem Betreff, einem Wert und einem Buchungsdatum. Der Klasse AccountEntry
ist ein leerer Default-Konstruktor hinzugefügt worden.AccountEntry[] getAccountEntries(String id)
: Diese Methode gibt alle Kontoeinträge zu einem Konto als Array zurück. Da die Bibliothek JAXB (Java Architecture for XML Binding) nicht ohne Weiteres Schnittstellen wie z.B. java.util.List
verarbeiten kann, ist das Return-Argument der Methode ein Array. JAXB übernimmt in der Java-Implementierung (s. unten) implizit die Serialisierung von Java-Objekten in eine XML-Repräsentation und die Deserialisierung in die andere Richtung.int getAccountBalance(String id)
: Diese Methode gibt den Saldo (= Summe der Werte aller Kontoeinträge) eines Kontos zurück.Es folgen entfernte Methodenaufrufe mittels SOAP für die ersten beiden Methoden des Webservice, nämlich String createAccount(String: owner)
und void addAccountEntry(String id, AccountEntry entry)
. Es wird jeweils exemplarisch ein HTTP-Request und die zugehörige HTTP-Response dargestellt.
Der Vorteil von SOAP ist, dass APIs für nahezu jede geläufige Programmiersprache existieren. Dadurch ist es möglich, dass Client und Server unabhängig voneinander in verschiedenen Programmiersprachen implementiert werden und trotzdem Nachrichten untereinander austauschen können. Die auszutauschenden Daten werden dazu automatisiert in SOAP-Nachrichten verpackt und mittels HTTP/HTTPS ausgetauscht. Jede SOAP-Nachricht ist ein gültiges XML-Dokument, das gegenüber einem zugehörigen XML-Schema validiert werden kann (vgl. Zeile 10: Referenz auf XML-Schema von SOAP Version 1.1 unter http://schemas.xmlsoap.org/soap/envelope/). Demzufolge enthält jede SOAP-Nachricht als Wurzelelement einen Envelope, der als Kindelemente einen optionalen Header und einen Body umfasst. Der Body einer SOAP-Nachricht enthält den eigentlichen Methodenaufruf inkl. der Argumente.
Als nächstes betrachten wir den generischen Begriff Webservice, der im Allgemeinen einen nicht weiter spezifizierten Mechanismus zur Maschine-zu-Maschine-Kommunikation auf Basis von HTTP/HTTPS über eine Netzwerkverbindung beschreibt. Während eine Webanwendung also ein User Interface besitzt, das im Browser dargestellt wird, bietet ein Webservice nur eine API ohne UI. Wenn SOAP als Protokoll für einen Webservice eingesetzt wird, sprechen wir explizit von einem SOAP-Webservice. Das W3C konzentriert sich in der folgenden Definition für Webservices auf genau diese SOAP-Variante, wobei auch auf alternative Webservice-Implementierungen hingewiesen wird.
Der Definition ist zu entnehmen, dass die Schnittstelle zwischen Client und Server im Falle eines SOAP-Webservice in einem strukturierten Format namens WSDL (Web Service Description Language) beschrieben wird. WSDL ist ebenfalls ein W3C-Standard, der im Wesentlichen auf XML-Schemas basiert. Ein WSDL-Dokument wird vom Webservice-Anbieter unter einer URI veröffentlicht. Es wird i.d.R. generiert und nicht händisch erstellt. Ein Client kann das WSDL-Dokument parsen und sich passende DTO-Klassen erzeugen lassen, um anschließend gültige SOAP-Nachrichten an den Webservice-Anbieter senden zu können. Es folgt ein Beispiel für ein WSDL-Dokument, das die Methoden des Webservice für das obige Anwendungsbeispiel beschreibt.
xmlns="http://schemas.xmlsoap.org/wsdl/"
).AccounEntry
) deklariert, ist oben im zweiten Tab dargestellt.In Java wird durch den JSR 224 eine API namens JAX-WS für SOAP-Webservices definiert, deren Referenzimplementierung (RI) das Framework JAX-WS RI ist. Maintainer für JAX-WS ist seit dem Entfernen der Java EE-Module aus dem JDK das Projekt Metro. Wir werden im Folgenden sowohl ein Client-Modul als auch ein Server-Modul mittels des Frameworks JAX-WS RI implementieren, so dass beide Module eine Abhängigkeit zum JAX-WS RI Runtime Bundle aufweisen werden. Die benötigten Annotationen von JAX-WS befinden sich im Package javax.jws
. Es folgt zunächst der Code des Server-Moduls.
package impl;
import javax.xml.ws.Endpoint;
public class WebServiceStarter {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.xml.ws.transport.http.HttpAdapter.dump", "true"); // dump SOAP messages to console
Endpoint endpoint = Endpoint.publish("http://localhost:8080/AccountService", new AccountServiceImpl());
System.out.println("Hit enter to stop web service endpoint.");
System.in.read();
endpoint.stop();
}
}
package impl;
import javax.jws.WebService;
import javax.jws.WebMethod;
import javax.jws.WebParam;
// ...
@WebService(name = "AccountService", serviceName = "AccountServiceFactory")
@SOAPBinding(style = SOAPBinding.Style.RPC)
public class AccountServiceImpl {
List<Account> accounts = new ArrayList<>();
@WebMethod
public String createAccount(@WebParam(name = "owner") String owner) {
Account account = new Account(owner);
accounts.add(account);
return account.id.toString();
}
@WebMethod
public void addAccountEntry(@WebParam(name = "accountId") String id, @WebParam(name = "entry") AccountEntry entry)
throws AccountNotFoundException {
getAccountById(id).entries.add(entry);
}
@WebMethod(operationName = "getAccountEntries")
public AccountEntry[] getEntries(@WebParam(name = "accountId") String id) throws AccountNotFoundException {
List<AccountEntry> entries = getAccountById(id).entries; // JAXB can't process interfaces
return entries.toArray(new AccountEntry[entries.size()]);
}
@WebMethod
public int getAccountBalance(@WebParam(name = "accountId") String id) throws AccountNotFoundException {
return Stream.of(getEntries(id)).mapToInt(entry -> entry.value).sum();
}
@WebMethod(exclude = true)
public void deleteAccount(@WebParam(name = "accountId") String id) throws AccountNotFoundException {
accounts.remove(getAccountById(id));
}
Account getAccountById(String id) throws AccountNotFoundException {
return accounts.stream().filter(a -> a.id.toString().equals(id)).findFirst()
.orElseThrow(() -> new AccountNotFoundException());
}
}
package dto;
// ...
public class Account {
UUID id;
String owner;
List<AccountEntry> entries;
public Account(String owner) {
this.id = UUID.randomUUID();
this.owner = owner;
entries = new ArrayList<>();
}
}
package dto;
// ...
public class AccountEntry {
String subject;
int value;
Date date;
public AccountEntry() { this.date = new Date(); }
public AccountEntry(String subject, int value) {
super();
this.subject = subject;
this.value = value;
}
}
package dto;
public class AccountNotFoundException extends Exception { }
WebServiceStarter
: In Zeile 10 wird der Webservice gestartet und unter dem Endpunkt http://localhost:8080/AccountService veröffentlicht. In Zeile 13 wird der Webservice wieder beendet. Die Systemeigenschaft, die in Zeile 8 aktiviert wird, sorgt lediglich dafür, dass alle ausgetauschten SOAP-Nachrichten auf die Konsole ausgegeben werden.AccountServiceImpl
: Die Klasse implementiert den SOAP-Webservice. Im Gegensatz zu RMI ist ein zugehöriges Interface, das dem Client zur Verfügung gestellt werden muss, nicht unbedingt erforderlich. Jede Klasse, die als Webservice instantiiert werden soll, benötigt die Annotation Webservice
(Zeile 8).@SOAPBinding(style = SOAPBinding.Style.RPC)
sorgt u.a. dafür, dass im generierten WSDL-Dokument Basisdatentypen wie Integer und String auf die entsprechenden Basisdatentypen von XML-Schema abgebildet werden, also z.B. xsd:int
und xsd:string
.@WebMethod(exclude = true)
annotiert. Private Methoden werden nicht in das WSDL-Dokument aufgenommen. Aus eben diesen Gründen sind die Methoden deleteAccount
(Zeilen 38-41) und getAccountById
(Zeilen 43-46) nicht im obigen WSDL-Dokument wiederzufinden, während alle anderen Methoden der Klasse AccountServiceImpl
im WSDL-Dokument enthalten sind.@WebMethod(operationName = "...")
und @WebParam(name = "...")
können Methoden bzw. ihre Argumente gezielt nach außen gerichtet bezeichnet werden. Im obigen WSDL-Dokument wird z.B. die Methode getEntries
exemplarisch in getAccountEntries
umbenannt (Zeilen 27-28). Die Methodenargumente explizit zu bezeichnen, ist bei JAX-WS RI sinnvoll, da diese im WSDL-Dokument andernfalls schlicht arg0
, arg1
, ... heißen.Account
, AccountEntry
und AccountNotFoundException
: Die serverseitigen DTO-Klassen haben sich im Vergleich zum RMI-Anwendungsbeispiel nicht grundlegend geändert. Das Interface java.io.Serializable
muss nicht mehr implementiert werden, da die Serialisierung und Deserialisierung in das XML-Format nun über die Bibliothek JAXB erfolgt.Die gezeigten Code-Beispiele zu SOAP-Webservices finden sich im Verzeichnis /remote/soap des Modul-Repository.
Damit ein Client diesen Webservice nutzen kann, benötigt er passende DTO-Klassen. JAX-WS sieht vor, dass diese DTO-Klassen mittels des Kommandozeilenwerkzeugs wsimport
generiert werden. Bis Java 10 war wsimport
Teil des JDK, seit Java 11 ist es zusammen mit den Java EE-Modulen ausgelagert worden. Heute kann es auf der Projektseite von JAX-WS heruntergeladen werden. Die folgende Abbildung illustriert die Generierung der DTO-Klassen für einen Client mittels wsimport
. Die Parameter werden in der Dokumentation zu wsimport
genauer erläutert.
Anschließend kann der Client die generierten DTO-Klassen verwenden, um SOAP-Nachrichten an den Webservice zu senden und Antworten zu empfangen. Im folgenden Code des Clients wird das Stub-Objekt in Zeile 12 erzeugt. Auf diesem Stub-Objekt werden danach die folgenden RPCs ausgeführt: Es wird ein neues Konto angelegt (Zeile 14), neue Kontoeinträge hinzugefügt (Zeilen 16-24) und der Saldo und die Anzahl der Kontoeinträge abgefragt (Zeilen 26-27).
package client;
import client.dto.AccountService;
import client.dto.AccountServiceFactory;
import client.dto.AccountEntry;
// ...
public class Client {
public static void main(String[] args) {
try {
AccountService stub = new AccountServiceFactory().getAccountServicePort(); // create remote proxy
String id = stub.createAccount("Hendricks");
AccountEntry e1 = new AccountEntry();
e1.setSubject("Credit 1");
e1.setValue(50);
stub.addAccountEntry(id, e1);
AccountEntry e2 = new AccountEntry();
e2.setSubject("Credit 2");
e2.setValue(30);
stub.addAccountEntry(id, e2);
System.out.println("Account balance = " + stub.getAccountBalance(id));
System.out.println("Account entries = " + stub.getAccountEntries(id).getItem().size());
} catch (AccountNotFoundException_Exception e) {
e.printStackTrace();
}
}
}
Bezüglich der generierten DTO-Klassen ist zu beachten, dass diese nicht 1:1 den serverseitigen DTO-Klassen entsprechen. Es fehlen z.B. geeignete Konstruktoren, obwohl diese in den serverseitigen DTO-Klassen vorhanden sind. So müssen im obigen Code z.B. die Attribute zu einem Kontoeintrag einzeln gesetzt werden (Zeilen 17-18), weil kein entsprechender Konstruktor generiert worden ist. Es bietet sich an, die generierten DTO-Klassen über einen Adapter an die Bedürfnisse des Clients anzupassen.
In der folgenden Abbildung wird der Ablauf der Kommunikation bei SOAP-Webservices (hier speziell für JAX-WS) zusammengefasst:javax.xml.ws.Endpoint.publish(...)
.wsimport
.SOAP-Webservices sind unabhängig von einer konkreten Laufzeitumgebung oder Programmiersprache. Das ist ein gewichtiger Vorteil zur Entkopplung der Komponenten in einem komplexen Softwaresystem. Der Preis dafür ist ein Performance-Nachteil gegenüber Protokollen wie RMI, die eine effizientere, binäre Serialisierung unterstützen. Bei starker Last kann mit RMI ein höherer Durchsatz an Nachrichten erreicht werden als mit SOAP-Webservices, da bei letzteren sämtliche Datentransferobjekte stets zwischen ihrer objektorientierten Repräsentation und XML transformiert werden müssen. Ob die Entkopplung von Komponenten oder die Maximierung des Nachrichtendurchsatzes im Fokus stehen, hängt von den konkreten Anforderungen eines Projekts ab.
Im Kontext von SOAP und WSDL sind noch diverse weitere W3C/OASIS-Spezifikationen – insbesondere zur sicheren und zuverlässigen Kommunikation über Organisationsgrenzen hinaus – entstanden, die wir heute zusammenfassend und durchaus etwas despektierlich als WS-* bezeichnen. Diese Spezifikationen wie WS-Security, WS-SecureConversation, WS-ReliableMessaging, WS-Policy, WS-Discovery, usw. nehmen der grundlegenden Idee "XML über HTTP" ihre Einfachheit und haben zu einer Komplexität geführt, die zusammen mit dem Datenformat JSON das Aufkommen von REST Webservices (s. nächstes Kapitel) als populäre Alternative zu SOAP begünstigte. SOAP-Webservices sind in großen Unternehmen weiterhin verbreitet, da sie lange von Marktführern wie Microsoft und IBM vorangetrieben worden sind. Letztlich gilt aber auch für den Entwurf von Webservices, dass nach einer einfachen Klarheit in der technischen Lösung gestrebt wird. Keith Ballinger vergleicht die historische Entwicklung der Wahrnehmung von SOAP und REST unter Software-Entwicklern in seinem Artikel "Simplicity and Utility, or, Why SOAP Lost" sehr anschaulich und endet mit: "Simple is important. Perhaps the most important thing. [...] YAGNI trumps future extensibility." YAGNI (You Aren’t Gonna Need It) ist ein bekanntes Prinzip des Extreme Programming (XP), das besagt Funktionalität erst zu implementieren, wenn sie auch benötigt wird.
Ähnlich der Registry bei RMI war auch für SOAP-Webservices ein zentraler Verzeichnisdienst vorgesehen, über den Webservices publiziert werden sollten. Die grundlegende Idee war, dass sich aus diesem Verzeichnisdienst ein weltweiter Marktplatz für Webservices ähnlich des Apple App Store oder des Google Play Store für mobile Anwendungen entwickelt. Dieser Verzeichnisdienst hieß UDDI (Universal Description, Discovery and Integration), hat sich in der Praxis aber nicht durchgesetzt. Bereits 2005 haben die initialen Unterstützer IBM, Microsoft und SAP ihren UDDI-Verzeichnisdienst geschlossen.