Beispiel: BillingRegistrationFault + BillingFaultDetail in novaris-common-legacy,
geworfen von BillingServiceEndpoint, behandelt in BillingIntegrationBean.
Ein SOAP Fault ist das SOAP-Aequivalent einer Exception - ein <soap:Fault>-Element in der
Antwort, statt eines regulaeren Ergebnis-Elements. JAX-WS bildet das direkt auf
Java-Exceptions ab:
@WebFault(name = "BillingRegistrationFault", targetNamespace = "http://billing.novaris.com/soap")
public class BillingRegistrationFault extends Exception {
private final BillingFaultDetail faultInfo;
// ...
public BillingFaultDetail getFaultInfo() { return faultInfo; }
}
@WebFault markiert eine checked Exception als SOAP-Fault-faehig. Wirft die
Server-Implementierung sie, serialisiert die JAX-WS-Laufzeit automatisch
getFaultInfo() in das Fault-XML; der Client deserialisiert es beim Empfang zurueck in
dieselbe Exception-Klasse (containerintern, voellig transparent fuer den aufrufenden Code -
er sieht einfach eine ganz normale catch (BillingRegistrationFault e)-Klausel).
Genau wie bei EJB (siehe docs/10-ejb-konzepte/06-exceptions.md) lohnt sich die
Unterscheidung zwischen einem fachlichen und einem technischen Fehler - nur ist der
Mechanismus bei SOAP ein anderer:
| Fachlicher Fehler | Technischer Fehler | |
|---|---|---|
| Beispiel | Police bereits registriert, ungueltige Praemie | Netzwerkausfall, Timeout, Server nicht erreichbar |
| Java-Typ | BillingRegistrationFault (checked, @WebFault) |
javax.xml.ws.WebServiceException (unchecked) |
| Auf der Wire-Ebene | strukturiertes <soap:Fault> mit Detail-XML |
i.d.R. gar keine SOAP-Antwort (Verbindung schlaegt fehl) |
| Sinnvolle Reaktion | fachlich auswerten (reasonCode), ggf. anders behandeln |
erneut versuchen, dann als Systemausfall behandeln |
BillingIntegrationBean behandelt beide bewusst unterschiedlich: ein
BillingRegistrationFault wird sofort ausgewertet (kein Retry - andernfalls wuerde man
denselben fachlichen Fehler nur wiederholt provozieren), ein WebServiceException wird
einmal wiederholt, bevor aufgegeben wird.
BillingFaultDetail.reasonCode unterscheidet zwei Faelle, die derselbe Exception-Typ
transportiert, aber die vollkommen unterschiedlich behandelt werden:
"ALREADY_REGISTERED" - kein echter Fehler, sondern ein Hinweis auf einen bereits
erledigten Zustand (z.B. eine reaktivierte, zuvor schon einmal registrierte Police).
BillingIntegrationBean behandelt das idempotent: sie liefert die im Fault
mitgelieferte, bereits existierende Kontonummer zurueck, statt einen Fehler zu propagieren."INVALID_PREMIUM" - ein echter Programmier-/Datenfehler auf Novaris-Seite (die
Praemie sollte zu diesem Zeitpunkt im Ablauf laengst korrekt berechnet sein). Wird als
IllegalStateException weitergereicht - ein Hinweis, dass hier etwas grundsaetzlich
falsch lief, keine erwartbare fachliche Situation.Der Lehrpunkt: ein einzelner Fault-Typ mit einem "reasonCode"-Unterscheidungsfeld ist
in der Praxis haeufiger als ein eigener Fault-Typ pro Fehlerfall - er haelt die WSDL
uebersichtlich, verlagert aber die Verantwortung, die einzelnen Faelle korrekt
auseinanderzuhalten, auf den aufrufenden Code (siehe handleBusinessFault(...) in
BillingIntegrationBean).
Diese Fault-Behandlung funktioniert nur, weil der Aufruf synchron ist - der Aufrufer
wartet auf die Antwort und kann sofort entscheiden. Genau das ist zugleich das im
BillingIntegrationBean-Javadoc beschriebene Risiko (synchroner externer Aufruf innerhalb
einer CMT-Transaktion). Eine asynchrone Entkopplung (siehe Phase 3,
docs/00-ueberblick/systemlandschaft.md) wuerde diese Art der direkten Fault-Behandlung
durch ein anderes Muster ersetzen muessen (z.B. Antwort-Queue, Kompensation) - ein guter
Vorgeschmack darauf, warum "SOAP synchron aufrufen" und "zuverlaessig asynchron
integrieren" zwei grundverschiedene Architekturprobleme sind.