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.

SOAP

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:

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.

Webservices und WSDL

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.

"There are many things that might be called 'web services' in the world at large. However, [...] we will use the following definition: A web service is a software system designed to support interoperable machine-to-machine interaction over a network. It has an interface described in a machine-processable format (specifically WSDL). Other systems interact with the web service in a manner prescribed by its description using SOAP-messages, typically conveyed using HTTP with an XML serialization in conjunction with other web-related standards." (W3C)

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.

Zusammenfassend sind folgende Aspekte bezüglich des WSDL-Formats bemerkenswert:

JAX-WS (Java API for XML Web Services)

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 { }

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:
  1. Der Anbieter (= Server) veröffentlicht seine Webservice-Implementierung unter einer bestimmten URI als Endpunkt – bei JAX-WS z.B. mittels javax.xml.ws.Endpoint.publish(...).
  2. Ein Client greift auf das veröffentlichte WSDL-Dokument zu, um sich passende Zugriffsklassen generieren zu lassen – bei JAX-WS z.B. mittels wsimport.
  3. Der Client sendet mit Hilfe der generierten Zugriffsklassen SOAP-Nachrichten im XML-Format an den Server und wartet anschließend auf eine SOAP-Nachricht als Antwort des Servers. SOAP-Webservices sind auf synchrone Kommunikation ausgelegt.

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.

WS-*

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.

Verzeichnisdienst UDDI

Ä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.